@substrate-system/actor-typeahead 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright (c) 2026 Jake Lazaroff\n *\n * Repository: https://tangled.org/jakelazaroff.com/actor-typeahead\n */\n\nimport { WebComponent } from '@substrate-system/web-component'\n\n// for document.querySelector\ndeclare global {\n interface HTMLElementTagNameMap {\n 'actor-typeahead':ActorTypeahead\n }\n}\n\nexport interface Actor {\n handle:string;\n avatar?:string;\n}\n\nconst DEFAULT_HOST = 'https://public.api.bsky.app'\nconst DEFAULT_ROWS = 5\n\n// The Suggestion list is a top-layer popover, positioned against the host\n// with CSS anchor positioning. `anchor-scope` shipped last of that set, so\n// it gates the group. Without it, the old absolute layout is used.\nconst ANCHORED = typeof CSS !== 'undefined' &&\n CSS.supports('anchor-scope: --a')\n\n// counts instances, so each one gets unique element ids\nlet instances = 0\n\n/**\n * @attribute {string} [host] - The host to which to make the typeahead\n * API call.\n * @attribute {number} [rows] - The maximum number of Suggestions to\n * display in the Suggestion list.\n *\n * @fires actor-typeahead:select - A Suggestion was chosen from the\n * Suggestion list. The event `detail` is `{ handle, avatar }`.\n *\n * @cssprop --color-background - Controls the color of the Suggestion list\n * background.\n * @cssprop --color-border - Controls the color of the Suggestion list\n * border.\n * @cssprop --color-shadow - Controls the color of the Suggestion list\n * shadow.\n * @cssprop --color-hover - Controls the background color of each\n * Suggestion on hover.\n * @cssprop --color-avatar-fallback - Controls the background color of an\n * avatar circle if the image fails to load.\n * @cssprop --radius - Controls the corner radius of the Suggestion list.\n * @cssprop --padding-suggestion-list - Controls the padding of the\n * Suggestion list.\n *\n * @summary A small web component that progressively enhances an <input>\n * element into an autocomplete for ATProto handles.\n *\n * @tag actor-typeahead\n */\nexport class ActorTypeahead extends WebComponent.create('actor-typeahead') {\n static reflectedStringAttributes = ['host', 'rows']\n\n declare host:string|null\n declare rows:string|null\n\n #id = `actor-typeahead-${++instances}`\n #actors:Actor[] = []\n #index = -1\n #pressed = false\n #suggestionList:HTMLUListElement|null = null\n\n constructor () {\n super()\n this.addEventListener('input', this)\n this.addEventListener('focusout', this)\n this.addEventListener('keydown', this)\n }\n\n get #limit ():number {\n const rows = Number.parseInt(this.rows ?? '')\n return Number.isNaN(rows) ? DEFAULT_ROWS : rows\n }\n\n handleEvent (evt:Event) {\n switch (evt.type) {\n case 'input':\n this.#oninput(evt)\n break\n\n case 'keydown':\n if (evt instanceof KeyboardEvent) this.#onkeydown(evt)\n break\n\n case 'focusout':\n this.#onfocusout()\n break\n\n case 'pointerdown':\n this.#pressed = true\n break\n\n case 'pointerup':\n this.#onpointerup(evt)\n break\n }\n }\n\n #onkeydown (evt:KeyboardEvent) {\n const last = this.#actors.length - 1\n\n switch (evt.key) {\n case 'ArrowDown':\n evt.preventDefault()\n this.#index = Math.min(this.#index + 1, last)\n this.render()\n break\n\n case 'PageDown':\n evt.preventDefault()\n this.#index = last\n this.render()\n break\n\n case 'ArrowUp':\n evt.preventDefault()\n this.#index = Math.max(this.#index - 1, 0)\n this.render()\n break\n\n case 'PageUp':\n evt.preventDefault()\n this.#index = 0\n this.render()\n break\n\n case 'Escape':\n evt.preventDefault()\n this.#clear()\n break\n\n case 'Enter':\n evt.preventDefault()\n this.#select(this.#index)\n break\n }\n }\n\n async #oninput (evt:Event) {\n if (!(evt.target instanceof HTMLInputElement)) return\n\n const query = evt.target.value\n if (!query) {\n this.#clear()\n return\n }\n\n const url = new URL(\n 'xrpc/app.bsky.actor.searchActorsTypeahead',\n this.host ?? DEFAULT_HOST\n )\n url.searchParams.set('q', query)\n url.searchParams.set('limit', String(this.#limit))\n\n const res = await fetch(url)\n const json:unknown = await res.json()\n this.#actors = parseActors(json)\n this.#index = -1\n this.render()\n }\n\n #onfocusout () {\n if (this.#pressed) return\n this.#clear()\n }\n\n #onpointerup (evt:Event) {\n this.#pressed = false\n\n const option = evt.target instanceof Element ?\n evt.target.closest<HTMLElement>('[role=\"option\"]') :\n null\n\n if (!option) {\n this.qs('input')?.focus()\n return\n }\n\n this.#select(Number(option.dataset.index))\n }\n\n #select (index:number) {\n const actor = this.#actors[index]\n const input = this.qs('input')\n if (!actor || !input) return\n\n input.value = actor.handle\n input.focus()\n this.#clear()\n this.emit<Actor>('select', { detail: actor })\n }\n\n #clear () {\n this.#actors = []\n this.#index = -1\n this.render()\n }\n\n /**\n * Render into light DOM, next to the authored `<input>`. Only the\n * Suggestion list is rebuilt, so the input element, and anything the\n * consumer attached to it, is never replaced. On the input, only the\n * ARIA combobox attributes are written, plus `autocomplete=\"off\"`\n * when the page did not set `autocomplete`.\n */\n render () {\n if (!this.#suggestionList) {\n this.#suggestionList = document.createElement('ul')\n this.#suggestionList.className = 'suggestion-list'\n this.#suggestionList.id = `${this.#id}-list`\n this.#suggestionList.setAttribute('role', 'listbox')\n if (ANCHORED) this.#suggestionList.popover = 'manual'\n this.#suggestionList.addEventListener('pointerdown', this)\n this.#suggestionList.addEventListener('pointerup', this)\n }\n\n const list = this.#suggestionList\n if (list.parentNode !== this) this.append(list)\n\n list.replaceChildren(...this.#actors.map((actor, i) => {\n const option = suggestion(actor, i, i === this.#index)\n option.id = this.#optionId(i)\n return option\n }))\n\n const open = this.#toggle(list, this.#actors.length > 0)\n\n const input = this.qs('input')\n if (input) {\n input.setAttribute('role', 'combobox')\n input.setAttribute('aria-autocomplete', 'list')\n if (!input.hasAttribute('autocomplete')) {\n input.setAttribute('autocomplete', 'off')\n }\n input.setAttribute('aria-controls', list.id)\n input.setAttribute('aria-expanded', String(open))\n\n const labels = [...(input.labels ?? [])]\n .map(label => label.id)\n .filter(Boolean)\n if (labels.length) {\n list.setAttribute('aria-labelledby', labels.join(' '))\n } else {\n list.removeAttribute('aria-labelledby')\n }\n\n if (this.#actors[this.#index]) {\n input.setAttribute(\n 'aria-activedescendant',\n this.#optionId(this.#index)\n )\n } else {\n input.removeAttribute('aria-activedescendant')\n }\n }\n }\n\n /**\n * Show or hide the Suggestion list popover, guarded on its current\n * state so repeated renders neither throw nor toggle. A disconnected\n * popover cannot be shown, e.g. when a response arrives after the host\n * was removed. Returns whether the Suggestion list is open.\n */\n #toggle (list:HTMLElement, open:boolean):boolean {\n if (!ANCHORED) return open\n if (!list.isConnected) return false\n\n const isOpen = list.matches(':popover-open')\n if (open && !isOpen) list.showPopover()\n if (!open && isOpen) list.hidePopover()\n return list.matches(':popover-open')\n }\n\n #optionId (index:number):string {\n return `${this.#id}-option-${index}`\n }\n}\n\nActorTypeahead.define()\n\n/**\n * Build one Suggestion. Values from the API go through `textContent` and\n * properties, never through `innerHTML`.\n */\nfunction suggestion (\n actor:Actor,\n index:number,\n active:boolean\n):HTMLLIElement {\n const li = document.createElement('li')\n li.className = 'suggestion'\n li.setAttribute('role', 'option')\n li.setAttribute('aria-selected', String(active))\n li.dataset.index = String(index)\n li.dataset.handle = actor.handle\n\n const avatar = document.createElement('div')\n avatar.className = 'avatar'\n\n const img = document.createElement('img')\n img.className = 'img'\n img.alt = ''\n if (actor.avatar) img.src = actor.avatar\n\n const handle = document.createElement('span')\n handle.className = 'handle'\n handle.textContent = actor.handle\n\n avatar.append(img)\n li.append(avatar, handle)\n\n return li\n}\n\nfunction parseActors (json:unknown):Actor[] {\n if (!isRecord(json) || !Array.isArray(json.actors)) return []\n\n return json.actors.flatMap((actor:unknown) => {\n if (!isRecord(actor) || typeof actor.handle !== 'string') return []\n\n return [typeof actor.avatar === 'string' ?\n { handle: actor.handle, avatar: actor.avatar } :\n { handle: actor.handle }]\n })\n}\n\nfunction isRecord (value:unknown):value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright (c) 2026 Jake Lazaroff\n *\n * Repository: https://tangled.org/jakelazaroff.com/actor-typeahead\n */\n\nimport { WebComponent } from '@substrate-system/web-component'\n\n// for document.querySelector\ndeclare global {\n interface HTMLElementTagNameMap {\n 'actor-typeahead':ActorTypeahead\n }\n}\n\nexport interface Actor {\n handle:string;\n avatar?:string;\n}\n\nconst DEFAULT_HOST = 'https://public.api.bsky.app'\nconst DEFAULT_ROWS = 5\n\n// how long the Query must stay unchanged before a Typeahead request\nconst DEBOUNCE_MS = 150\n\n// The Suggestion list is a top-layer popover, positioned against the host\n// with CSS anchor positioning. `anchor-scope` shipped last of that set, so\n// it gates the group. Without it, the old absolute layout is used.\nconst ANCHORED = typeof CSS !== 'undefined' &&\n CSS.supports('anchor-scope: --a')\n\n// counts instances, so each one gets unique element ids\nlet instances = 0\n\n/**\n * @attribute {string} [host] - The host to which to make the typeahead\n * API call.\n * @attribute {number} [rows] - The maximum number of Suggestions to\n * display in the Suggestion list.\n *\n * @fires actor-typeahead:select - A Suggestion was chosen from the\n * Suggestion list. The event `detail` is `{ handle, avatar }`.\n *\n * @cssprop --color-background - Controls the color of the Suggestion list\n * background.\n * @cssprop --color-border - Controls the color of the Suggestion list\n * border.\n * @cssprop --color-shadow - Controls the color of the Suggestion list\n * shadow.\n * @cssprop --color-hover - Controls the background color of each\n * Suggestion on hover.\n * @cssprop --color-avatar-fallback - Controls the background color of an\n * avatar circle if the image fails to load.\n * @cssprop --radius - Controls the corner radius of the Suggestion list.\n * @cssprop --padding-suggestion-list - Controls the padding of the\n * Suggestion list.\n *\n * @summary A small web component that progressively enhances an <input>\n * element into an autocomplete for ATProto handles.\n *\n * @tag actor-typeahead\n */\nexport class ActorTypeahead extends WebComponent.create('actor-typeahead') {\n static reflectedStringAttributes = ['host', 'rows']\n\n declare host:string|null\n declare rows:string|null\n\n /**\n * Returns the text announced when a Typeahead request settles with\n * `count` Suggestions, including 0 when nothing matched the Query.\n * Defaults to English. Replace it to translate; the replacement\n * handles plurals and 0. Read at announce time, so it can be set\n * after connect.\n */\n announce:(count:number) => string = defaultAnnounce\n\n #id = `actor-typeahead-${++instances}`\n #actors:Actor[] = []\n #index = -1\n #suggestionList:HTMLDivElement|null = null\n #suggestionCount:HTMLDivElement|null = null\n #nbsp = false\n #timer:ReturnType<typeof setTimeout>|undefined\n #controller:AbortController|null = null\n\n constructor () {\n super()\n this.addEventListener('input', this)\n this.addEventListener('focusout', this)\n this.addEventListener('keydown', this)\n }\n\n /**\n * Stop any pending Typeahead request. The Suggestion list is left as\n * is; the next Query rebuilds it.\n */\n disconnectedCallback () {\n this.#cancel()\n }\n\n get #limit ():number {\n const rows = Number.parseInt(this.rows ?? '')\n return Number.isNaN(rows) ? DEFAULT_ROWS : rows\n }\n\n handleEvent (evt:Event) {\n switch (evt.type) {\n case 'input':\n this.#oninput(evt)\n break\n\n case 'keydown':\n if (evt instanceof KeyboardEvent) this.#onkeydown(evt)\n break\n\n case 'focusout':\n if (evt instanceof FocusEvent) this.#onfocusout(evt)\n break\n\n case 'mousedown':\n // a press or a tap keeps focus on the Authored input\n evt.preventDefault()\n break\n\n case 'click':\n this.#onclick(evt)\n break\n }\n }\n\n #onkeydown (evt:KeyboardEvent) {\n const last = this.#actors.length - 1\n const moves = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']\n // with no Suggestions there is nothing to move to, and a render\n // would empty a \"No Suggestions\" announcement\n if (last < 0 && moves.includes(evt.key)) {\n evt.preventDefault()\n return\n }\n\n switch (evt.key) {\n case 'ArrowDown':\n evt.preventDefault()\n this.#index = Math.min(this.#index + 1, last)\n this.render()\n break\n\n case 'PageDown':\n evt.preventDefault()\n this.#index = last\n this.render()\n break\n\n case 'ArrowUp':\n evt.preventDefault()\n // wrap from nothing Highlighted to the last Suggestion,\n // and from the first back to nothing (the APG pattern)\n this.#index = this.#index < 0 ? last : this.#index - 1\n this.render()\n break\n\n case 'PageUp':\n evt.preventDefault()\n this.#index = 0\n this.render()\n break\n\n case 'Escape':\n // with the list closed, let a dialog close; still cancel\n // any pending Typeahead request\n if (this.#actors.length) evt.preventDefault()\n this.#clear()\n break\n\n case 'Enter':\n // with nothing Highlighted, let the form submit, and\n // drop any pending Typeahead request\n if (!this.#actors[this.#index]) {\n this.#cancel()\n break\n }\n evt.preventDefault()\n this.#select(this.#index)\n break\n }\n }\n\n #oninput (evt:Event) {\n if (!(evt.target instanceof HTMLInputElement)) return\n\n const query = evt.target.value\n if (!query) {\n this.#clear()\n return\n }\n\n // keep the current Suggestions on screen until the new response\n this.#cancel()\n this.#timer = setTimeout(() => this.#request(query), DEBOUNCE_MS)\n }\n\n async #request (query:string) {\n this.#timer = undefined\n const url = new URL(\n 'xrpc/app.bsky.actor.searchActorsTypeahead',\n this.host ?? DEFAULT_HOST\n )\n url.searchParams.set('q', query)\n url.searchParams.set('limit', String(this.#limit))\n\n const controller = new AbortController()\n this.#controller = controller\n const { signal } = controller\n\n let json:unknown\n try {\n const res = await fetch(url, { signal })\n if (signal.aborted) return\n if (!res.ok) throw new Error(`status ${res.status}`)\n json = await res.json()\n } catch (err) {\n // a newer Query, or a close, cancelled this request\n if (isAbortError(err)) return\n // any other failure closes the Suggestion list, quietly\n this.#clear()\n return\n }\n if (signal.aborted) return\n\n this.#actors = parseActors(json)\n this.#index = -1\n // after render(), which empties the count when there are none\n this.render()\n this.#announce(this.#actors.length)\n }\n\n /**\n * Write the Suggestion count. A trailing no-break space alternates\n * between writes, so screen readers re-read an identical count.\n */\n #announce (count:number) {\n if (!this.#suggestionCount) return\n this.#nbsp = !this.#nbsp\n this.#suggestionCount.textContent = this.announce(count) +\n (this.#nbsp ? '\\u00a0' : '')\n }\n\n /**\n * Close only when focus leaves the host. Some assistive technology\n * focuses a Suggestion before it sends the click that selects it.\n */\n #onfocusout (evt:FocusEvent) {\n const next = evt.relatedTarget\n if (next instanceof Node && this.contains(next)) return\n this.#clear()\n }\n\n #onclick (evt:Event) {\n const option = evt.target instanceof Element ?\n evt.target.closest<HTMLElement>('[role=\"option\"]') :\n null\n if (!option) return\n\n this.#select(Number(option.dataset.index))\n }\n\n #select (index:number) {\n const actor = this.#actors[index]\n const input = this.qs('input')\n if (!actor || !input) return\n\n input.value = actor.handle\n input.focus()\n this.#clear()\n this.emit<Actor>('select', { detail: actor })\n }\n\n /**\n * Stop any pending Typeahead request: the debounce timer, and the\n * request in flight, whose response is then dropped.\n */\n #cancel () {\n clearTimeout(this.#timer)\n this.#timer = undefined\n this.#controller?.abort()\n this.#controller = null\n }\n\n #clear () {\n this.#cancel()\n this.#actors = []\n this.#index = -1\n this.render()\n }\n\n /**\n * Render into light DOM. Only the suggestion list is rebuilt,\n * so the input element, and anything the consumer attached to it,\n * is never replaced. On the input, only the\n * ARIA combobox attributes are written, plus `autocomplete=\"off\"`\n * when the page did not set `autocomplete`.\n */\n render () {\n if (!this.#suggestionList) {\n this.#suggestionList = document.createElement('div')\n this.#suggestionList.className = 'suggestion-list'\n this.#suggestionList.id = `${this.#id}-list`\n this.#suggestionList.setAttribute('role', 'listbox')\n if (ANCHORED) this.#suggestionList.popover = 'manual'\n this.#suggestionList.addEventListener('mousedown', this)\n this.#suggestionList.addEventListener('click', this)\n }\n\n const list = this.#suggestionList\n if (list.parentNode !== this) this.append(list)\n\n // the live region exists empty before its first write, because\n // screen readers often skip a region inserted already filled\n if (!this.#suggestionCount) {\n this.#suggestionCount = document.createElement('div')\n this.#suggestionCount.className = 'suggestion-count'\n this.#suggestionCount.setAttribute('role', 'status')\n }\n const count = this.#suggestionCount\n if (count.parentNode !== this) this.append(count)\n // only a response writes the count; any close empties it, and a\n // response with no Suggestions writes it after this render\n if (!this.#actors.length) count.textContent = ''\n\n list.replaceChildren(...this.#actors.map((actor, i) => {\n const option = suggestion(actor, i, i === this.#index)\n option.id = this.#optionId(i)\n return option\n }))\n\n const open = this.#toggle(list, this.#actors.length > 0)\n\n const input = this.qs('input')\n if (input) {\n input.setAttribute('role', 'combobox')\n input.setAttribute('aria-autocomplete', 'list')\n if (!input.hasAttribute('autocomplete')) {\n input.setAttribute('autocomplete', 'off')\n }\n input.setAttribute('aria-controls', list.id)\n input.setAttribute('aria-expanded', String(open))\n\n // the Suggestion list carries at most one of the two\n const name = nameSource(input, this)\n for (const attr of ['aria-labelledby', 'aria-label'] as const) {\n const value = name[attr]\n if (value) list.setAttribute(attr, value)\n else list.removeAttribute(attr)\n }\n\n if (this.#actors[this.#index]) {\n input.setAttribute(\n 'aria-activedescendant',\n this.#optionId(this.#index)\n )\n } else {\n input.removeAttribute('aria-activedescendant')\n }\n }\n }\n\n /**\n * Show or hide the Suggestion list popover, guarded on its current\n * state so repeated renders neither throw nor toggle. A disconnected\n * popover cannot be shown, e.g. when a response arrives after the host\n * was removed. Returns whether the Suggestion list is open.\n */\n #toggle (list:HTMLElement, open:boolean):boolean {\n if (!ANCHORED) return open\n if (!list.isConnected) return false\n\n const isOpen = list.matches(':popover-open')\n if (open && !isOpen) list.showPopover()\n if (!open && isOpen) list.hidePopover()\n return list.matches(':popover-open')\n }\n\n #optionId (index:number):string {\n return `${this.#id}-option-${index}`\n }\n}\n\nActorTypeahead.define()\n\n/**\n * Build one Suggestion. Values from the API go through `textContent` and\n * properties, never through `innerHTML`.\n */\nfunction suggestion (\n actor:Actor,\n index:number,\n active:boolean\n):HTMLButtonElement {\n const button = document.createElement('button')\n // a button, so every input method can activate it, but not a\n // submit button, and out of the tab order, because focus stays on\n // the Authored input\n button.type = 'button'\n button.tabIndex = -1\n button.className = 'suggestion'\n button.setAttribute('role', 'option')\n button.setAttribute('aria-selected', String(active))\n button.dataset.index = String(index)\n button.dataset.handle = actor.handle\n\n const avatar = document.createElement('div')\n avatar.className = 'avatar'\n\n const img = document.createElement('img')\n img.className = 'img'\n img.alt = ''\n if (actor.avatar) img.src = actor.avatar\n\n const handle = document.createElement('span')\n handle.className = 'handle'\n handle.textContent = actor.handle\n\n avatar.append(img)\n button.append(avatar, handle)\n\n return button\n}\n\n/**\n * What names the Authored input, in the order of accessible name\n * computation, as the attribute the Suggestion list copies it to.\n */\nfunction nameSource (input:HTMLInputElement, host:HTMLElement):{\n 'aria-labelledby'?:string;\n 'aria-label'?:string;\n} {\n const labelledby = input.getAttribute('aria-labelledby')\n if (labelledby) return { 'aria-labelledby': labelledby }\n\n const label = input.getAttribute('aria-label')\n if (label) return { 'aria-label': label }\n\n // reference the labels when they all have ids, otherwise copy their\n // text, because the component never writes ids onto the page\n const labels = [...(input.labels ?? [])]\n if (!labels.length) return {}\n if (labels.every(label => label.id)) {\n return {\n 'aria-labelledby': labels.map(label => label.id).join(' ')\n }\n }\n\n const text = labels\n .map(label => labelText(label, host).trim())\n .filter(Boolean)\n .join(' ')\n return text ? { 'aria-label': text } : {}\n}\n\n/**\n * The text of a label, without the Host element's content, which a\n * label that wraps the Host element would otherwise include.\n */\nfunction labelText (label:HTMLLabelElement, host:HTMLElement):string {\n const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT, {\n acceptNode: node => host.contains(node) ?\n NodeFilter.FILTER_REJECT :\n NodeFilter.FILTER_ACCEPT\n })\n\n let text = ''\n while (walker.nextNode()) text += walker.currentNode.nodeValue\n return text\n}\n\n/**\n * The default English Suggestion count announcement.\n */\nfunction defaultAnnounce (count:number):string {\n if (count === 0) return 'No Suggestions'\n return count === 1 ?\n '1 Suggestion available' :\n `${count} Suggestions available`\n}\n\nfunction parseActors (json:unknown):Actor[] {\n if (!isRecord(json) || !Array.isArray(json.actors)) return []\n\n return json.actors.flatMap((actor:unknown) => {\n if (!isRecord(actor) || typeof actor.handle !== 'string') return []\n\n return [typeof actor.avatar === 'string' ?\n { handle: actor.handle, avatar: actor.avatar } :\n { handle: actor.handle }]\n })\n}\n\nfunction isAbortError (err:unknown):boolean {\n return err instanceof DOMException && err.name === 'AbortError'\n}\n\nfunction isRecord (value:unknown):value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["label"]
7
7
  }
package/dist/index.css CHANGED
@@ -1,6 +1,6 @@
1
1
  actor-typeahead {
2
2
  --color-background-inherited: var(--color-background, #fff);
3
- --color-border-inherited: var(--color-border, #0002);
3
+ --color-border-inherited: var(--color-border, #0007);
4
4
  --color-shadow-inherited: var(--color-shadow, #000);
5
5
  --color-hover-inherited: var(--color-hover, #0001);
6
6
  --color-avatar-fallback-inherited: var(--color-avatar-fallback, #0002);
@@ -29,7 +29,6 @@ actor-typeahead {
29
29
  background-clip: padding-box;
30
30
  flex-direction: column;
31
31
  margin-top: 4px;
32
- list-style: none;
33
32
  display: flex;
34
33
  position: absolute;
35
34
  left: 0;
@@ -57,6 +56,15 @@ actor-typeahead {
57
56
  }
58
57
  }
59
58
 
59
+ & .suggestion-count {
60
+ clip-path: inset(50%);
61
+ white-space: nowrap;
62
+ width: 1px;
63
+ height: 1px;
64
+ position: absolute;
65
+ overflow: hidden;
66
+ }
67
+
60
68
  & .suggestion {
61
69
  all: unset;
62
70
  box-sizing: border-box;
@@ -73,6 +81,19 @@ actor-typeahead {
73
81
  &:hover, &[aria-selected="true"] {
74
82
  background-color: var(--color-hover-inherited);
75
83
  }
84
+
85
+ &[aria-selected="true"] {
86
+ outline-offset: -2px;
87
+ outline: 2px solid;
88
+ }
89
+
90
+ @media (forced-colors: active) {
91
+ &[aria-selected="true"] {
92
+ forced-color-adjust: none;
93
+ color: highlighttext;
94
+ background-color: highlight;
95
+ }
96
+ }
76
97
  }
77
98
 
78
99
  & .avatar {
package/dist/index.d.ts CHANGED
@@ -49,12 +49,25 @@ export declare class ActorTypeahead extends ActorTypeahead_base {
49
49
  static reflectedStringAttributes: string[];
50
50
  host: string | null;
51
51
  rows: string | null;
52
+ /**
53
+ * Returns the text announced when a Typeahead request settles with
54
+ * `count` Suggestions, including 0 when nothing matched the Query.
55
+ * Defaults to English. Replace it to translate; the replacement
56
+ * handles plurals and 0. Read at announce time, so it can be set
57
+ * after connect.
58
+ */
59
+ announce: (count: number) => string;
52
60
  constructor();
61
+ /**
62
+ * Stop any pending Typeahead request. The Suggestion list is left as
63
+ * is; the next Query rebuilds it.
64
+ */
65
+ disconnectedCallback(): void;
53
66
  handleEvent(evt: Event): void;
54
67
  /**
55
- * Render into light DOM, next to the authored `<input>`. Only the
56
- * Suggestion list is rebuilt, so the input element, and anything the
57
- * consumer attached to it, is never replaced. On the input, only the
68
+ * Render into light DOM. Only the suggestion list is rebuilt,
69
+ * so the input element, and anything the consumer attached to it,
70
+ * is never replaced. On the input, only the
58
71
  * ARIA combobox attributes are written, plus `autocomplete="off"`
59
72
  * when the page did not set `autocomplete`.
60
73
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAG9D,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAC,cAAc,CAAA;KACnC;CACJ;AAED,MAAM,WAAW,KAAK;IAClB,MAAM,EAAC,MAAM,CAAC;IACd,MAAM,CAAC,EAAC,MAAM,CAAC;CAClB;;;;;;;;;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,cAAe,SAAQ,mBAAsC;;IACtE,MAAM,CAAC,yBAAyB,WAAmB;IAE3C,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;IAChB,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;;IAoBxB,WAAW,CAAE,GAAG,EAAC,KAAK;IA4HtB;;;;;;OAMG;IACH,MAAM;CAuET"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAG9D,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAC,cAAc,CAAA;KACnC;CACJ;AAED,MAAM,WAAW,KAAK;IAClB,MAAM,EAAC,MAAM,CAAC;IACd,MAAM,CAAC,EAAC,MAAM,CAAC;CAClB;;;;;;;;;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,cAAe,SAAQ,mBAAsC;;IACtE,MAAM,CAAC,yBAAyB,WAAmB;IAE3C,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;IAChB,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;IAExB;;;;;;OAMG;IACH,QAAQ,EAAC,CAAC,KAAK,EAAC,MAAM,KAAK,MAAM,CAAkB;;IAkBnD;;;OAGG;IACH,oBAAoB;IASpB,WAAW,CAAE,GAAG,EAAC,KAAK;IA8LtB;;;;;;OAMG;IACH,MAAM;CAmFT"}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
  import { WebComponent } from "@substrate-system/web-component";
4
4
  const DEFAULT_HOST = "https://public.api.bsky.app";
5
5
  const DEFAULT_ROWS = 5;
6
+ const DEBOUNCE_MS = 150;
6
7
  const ANCHORED = typeof CSS !== "undefined" && CSS.supports("anchor-scope: --a");
7
8
  let instances = 0;
8
9
  class ActorTypeahead extends WebComponent.create("actor-typeahead") {
@@ -10,17 +11,35 @@ class ActorTypeahead extends WebComponent.create("actor-typeahead") {
10
11
  __name(this, "ActorTypeahead");
11
12
  }
12
13
  static reflectedStringAttributes = ["host", "rows"];
14
+ /**
15
+ * Returns the text announced when a Typeahead request settles with
16
+ * `count` Suggestions, including 0 when nothing matched the Query.
17
+ * Defaults to English. Replace it to translate; the replacement
18
+ * handles plurals and 0. Read at announce time, so it can be set
19
+ * after connect.
20
+ */
21
+ announce = defaultAnnounce;
13
22
  #id = `actor-typeahead-${++instances}`;
14
23
  #actors = [];
15
24
  #index = -1;
16
- #pressed = false;
17
25
  #suggestionList = null;
26
+ #suggestionCount = null;
27
+ #nbsp = false;
28
+ #timer;
29
+ #controller = null;
18
30
  constructor() {
19
31
  super();
20
32
  this.addEventListener("input", this);
21
33
  this.addEventListener("focusout", this);
22
34
  this.addEventListener("keydown", this);
23
35
  }
36
+ /**
37
+ * Stop any pending Typeahead request. The Suggestion list is left as
38
+ * is; the next Query rebuilds it.
39
+ */
40
+ disconnectedCallback() {
41
+ this.#cancel();
42
+ }
24
43
  get #limit() {
25
44
  const rows = Number.parseInt(this.rows ?? "");
26
45
  return Number.isNaN(rows) ? DEFAULT_ROWS : rows;
@@ -34,18 +53,23 @@ class ActorTypeahead extends WebComponent.create("actor-typeahead") {
34
53
  if (evt instanceof KeyboardEvent) this.#onkeydown(evt);
35
54
  break;
36
55
  case "focusout":
37
- this.#onfocusout();
56
+ if (evt instanceof FocusEvent) this.#onfocusout(evt);
38
57
  break;
39
- case "pointerdown":
40
- this.#pressed = true;
58
+ case "mousedown":
59
+ evt.preventDefault();
41
60
  break;
42
- case "pointerup":
43
- this.#onpointerup(evt);
61
+ case "click":
62
+ this.#onclick(evt);
44
63
  break;
45
64
  }
46
65
  }
47
66
  #onkeydown(evt) {
48
67
  const last = this.#actors.length - 1;
68
+ const moves = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"];
69
+ if (last < 0 && moves.includes(evt.key)) {
70
+ evt.preventDefault();
71
+ return;
72
+ }
49
73
  switch (evt.key) {
50
74
  case "ArrowDown":
51
75
  evt.preventDefault();
@@ -59,7 +83,7 @@ class ActorTypeahead extends WebComponent.create("actor-typeahead") {
59
83
  break;
60
84
  case "ArrowUp":
61
85
  evt.preventDefault();
62
- this.#index = Math.max(this.#index - 1, 0);
86
+ this.#index = this.#index < 0 ? last : this.#index - 1;
63
87
  this.render();
64
88
  break;
65
89
  case "PageUp":
@@ -68,45 +92,78 @@ class ActorTypeahead extends WebComponent.create("actor-typeahead") {
68
92
  this.render();
69
93
  break;
70
94
  case "Escape":
71
- evt.preventDefault();
95
+ if (this.#actors.length) evt.preventDefault();
72
96
  this.#clear();
73
97
  break;
74
98
  case "Enter":
99
+ if (!this.#actors[this.#index]) {
100
+ this.#cancel();
101
+ break;
102
+ }
75
103
  evt.preventDefault();
76
104
  this.#select(this.#index);
77
105
  break;
78
106
  }
79
107
  }
80
- async #oninput(evt) {
108
+ #oninput(evt) {
81
109
  if (!(evt.target instanceof HTMLInputElement)) return;
82
110
  const query = evt.target.value;
83
111
  if (!query) {
84
112
  this.#clear();
85
113
  return;
86
114
  }
115
+ this.#cancel();
116
+ this.#timer = setTimeout(() => this.#request(query), DEBOUNCE_MS);
117
+ }
118
+ async #request(query) {
119
+ this.#timer = void 0;
87
120
  const url = new URL(
88
121
  "xrpc/app.bsky.actor.searchActorsTypeahead",
89
122
  this.host ?? DEFAULT_HOST
90
123
  );
91
124
  url.searchParams.set("q", query);
92
125
  url.searchParams.set("limit", String(this.#limit));
93
- const res = await fetch(url);
94
- const json = await res.json();
126
+ const controller = new AbortController();
127
+ this.#controller = controller;
128
+ const { signal } = controller;
129
+ let json;
130
+ try {
131
+ const res = await fetch(url, { signal });
132
+ if (signal.aborted) return;
133
+ if (!res.ok) throw new Error(`status ${res.status}`);
134
+ json = await res.json();
135
+ } catch (err) {
136
+ if (isAbortError(err)) return;
137
+ this.#clear();
138
+ return;
139
+ }
140
+ if (signal.aborted) return;
95
141
  this.#actors = parseActors(json);
96
142
  this.#index = -1;
97
143
  this.render();
144
+ this.#announce(this.#actors.length);
145
+ }
146
+ /**
147
+ * Write the Suggestion count. A trailing no-break space alternates
148
+ * between writes, so screen readers re-read an identical count.
149
+ */
150
+ #announce(count) {
151
+ if (!this.#suggestionCount) return;
152
+ this.#nbsp = !this.#nbsp;
153
+ this.#suggestionCount.textContent = this.announce(count) + (this.#nbsp ? "\xA0" : "");
98
154
  }
99
- #onfocusout() {
100
- if (this.#pressed) return;
155
+ /**
156
+ * Close only when focus leaves the host. Some assistive technology
157
+ * focuses a Suggestion before it sends the click that selects it.
158
+ */
159
+ #onfocusout(evt) {
160
+ const next = evt.relatedTarget;
161
+ if (next instanceof Node && this.contains(next)) return;
101
162
  this.#clear();
102
163
  }
103
- #onpointerup(evt) {
104
- this.#pressed = false;
164
+ #onclick(evt) {
105
165
  const option = evt.target instanceof Element ? evt.target.closest('[role="option"]') : null;
106
- if (!option) {
107
- this.qs("input")?.focus();
108
- return;
109
- }
166
+ if (!option) return;
110
167
  this.#select(Number(option.dataset.index));
111
168
  }
112
169
  #select(index) {
@@ -118,30 +175,49 @@ class ActorTypeahead extends WebComponent.create("actor-typeahead") {
118
175
  this.#clear();
119
176
  this.emit("select", { detail: actor });
120
177
  }
178
+ /**
179
+ * Stop any pending Typeahead request: the debounce timer, and the
180
+ * request in flight, whose response is then dropped.
181
+ */
182
+ #cancel() {
183
+ clearTimeout(this.#timer);
184
+ this.#timer = void 0;
185
+ this.#controller?.abort();
186
+ this.#controller = null;
187
+ }
121
188
  #clear() {
189
+ this.#cancel();
122
190
  this.#actors = [];
123
191
  this.#index = -1;
124
192
  this.render();
125
193
  }
126
194
  /**
127
- * Render into light DOM, next to the authored `<input>`. Only the
128
- * Suggestion list is rebuilt, so the input element, and anything the
129
- * consumer attached to it, is never replaced. On the input, only the
195
+ * Render into light DOM. Only the suggestion list is rebuilt,
196
+ * so the input element, and anything the consumer attached to it,
197
+ * is never replaced. On the input, only the
130
198
  * ARIA combobox attributes are written, plus `autocomplete="off"`
131
199
  * when the page did not set `autocomplete`.
132
200
  */
133
201
  render() {
134
202
  if (!this.#suggestionList) {
135
- this.#suggestionList = document.createElement("ul");
203
+ this.#suggestionList = document.createElement("div");
136
204
  this.#suggestionList.className = "suggestion-list";
137
205
  this.#suggestionList.id = `${this.#id}-list`;
138
206
  this.#suggestionList.setAttribute("role", "listbox");
139
207
  if (ANCHORED) this.#suggestionList.popover = "manual";
140
- this.#suggestionList.addEventListener("pointerdown", this);
141
- this.#suggestionList.addEventListener("pointerup", this);
208
+ this.#suggestionList.addEventListener("mousedown", this);
209
+ this.#suggestionList.addEventListener("click", this);
142
210
  }
143
211
  const list = this.#suggestionList;
144
212
  if (list.parentNode !== this) this.append(list);
213
+ if (!this.#suggestionCount) {
214
+ this.#suggestionCount = document.createElement("div");
215
+ this.#suggestionCount.className = "suggestion-count";
216
+ this.#suggestionCount.setAttribute("role", "status");
217
+ }
218
+ const count = this.#suggestionCount;
219
+ if (count.parentNode !== this) this.append(count);
220
+ if (!this.#actors.length) count.textContent = "";
145
221
  list.replaceChildren(...this.#actors.map((actor, i) => {
146
222
  const option = suggestion(actor, i, i === this.#index);
147
223
  option.id = this.#optionId(i);
@@ -157,11 +233,11 @@ class ActorTypeahead extends WebComponent.create("actor-typeahead") {
157
233
  }
158
234
  input.setAttribute("aria-controls", list.id);
159
235
  input.setAttribute("aria-expanded", String(open));
160
- const labels = [...input.labels ?? []].map((label) => label.id).filter(Boolean);
161
- if (labels.length) {
162
- list.setAttribute("aria-labelledby", labels.join(" "));
163
- } else {
164
- list.removeAttribute("aria-labelledby");
236
+ const name = nameSource(input, this);
237
+ for (const attr of ["aria-labelledby", "aria-label"]) {
238
+ const value = name[attr];
239
+ if (value) list.setAttribute(attr, value);
240
+ else list.removeAttribute(attr);
165
241
  }
166
242
  if (this.#actors[this.#index]) {
167
243
  input.setAttribute(
@@ -193,12 +269,14 @@ class ActorTypeahead extends WebComponent.create("actor-typeahead") {
193
269
  }
194
270
  ActorTypeahead.define();
195
271
  function suggestion(actor, index, active) {
196
- const li = document.createElement("li");
197
- li.className = "suggestion";
198
- li.setAttribute("role", "option");
199
- li.setAttribute("aria-selected", String(active));
200
- li.dataset.index = String(index);
201
- li.dataset.handle = actor.handle;
272
+ const button = document.createElement("button");
273
+ button.type = "button";
274
+ button.tabIndex = -1;
275
+ button.className = "suggestion";
276
+ button.setAttribute("role", "option");
277
+ button.setAttribute("aria-selected", String(active));
278
+ button.dataset.index = String(index);
279
+ button.dataset.handle = actor.handle;
202
280
  const avatar = document.createElement("div");
203
281
  avatar.className = "avatar";
204
282
  const img = document.createElement("img");
@@ -209,10 +287,40 @@ function suggestion(actor, index, active) {
209
287
  handle.className = "handle";
210
288
  handle.textContent = actor.handle;
211
289
  avatar.append(img);
212
- li.append(avatar, handle);
213
- return li;
290
+ button.append(avatar, handle);
291
+ return button;
214
292
  }
215
293
  __name(suggestion, "suggestion");
294
+ function nameSource(input, host) {
295
+ const labelledby = input.getAttribute("aria-labelledby");
296
+ if (labelledby) return { "aria-labelledby": labelledby };
297
+ const label = input.getAttribute("aria-label");
298
+ if (label) return { "aria-label": label };
299
+ const labels = [...input.labels ?? []];
300
+ if (!labels.length) return {};
301
+ if (labels.every((label2) => label2.id)) {
302
+ return {
303
+ "aria-labelledby": labels.map((label2) => label2.id).join(" ")
304
+ };
305
+ }
306
+ const text = labels.map((label2) => labelText(label2, host).trim()).filter(Boolean).join(" ");
307
+ return text ? { "aria-label": text } : {};
308
+ }
309
+ __name(nameSource, "nameSource");
310
+ function labelText(label, host) {
311
+ const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT, {
312
+ acceptNode: /* @__PURE__ */ __name((node) => host.contains(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, "acceptNode")
313
+ });
314
+ let text = "";
315
+ while (walker.nextNode()) text += walker.currentNode.nodeValue;
316
+ return text;
317
+ }
318
+ __name(labelText, "labelText");
319
+ function defaultAnnounce(count) {
320
+ if (count === 0) return "No Suggestions";
321
+ return count === 1 ? "1 Suggestion available" : `${count} Suggestions available`;
322
+ }
323
+ __name(defaultAnnounce, "defaultAnnounce");
216
324
  function parseActors(json) {
217
325
  if (!isRecord(json) || !Array.isArray(json.actors)) return [];
218
326
  return json.actors.flatMap((actor) => {
@@ -221,6 +329,10 @@ function parseActors(json) {
221
329
  });
222
330
  }
223
331
  __name(parseActors, "parseActors");
332
+ function isAbortError(err) {
333
+ return err instanceof DOMException && err.name === "AbortError";
334
+ }
335
+ __name(isAbortError, "isAbortError");
224
336
  function isRecord(value) {
225
337
  return typeof value === "object" && value !== null;
226
338
  }
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright (c) 2026 Jake Lazaroff\n *\n * Repository: https://tangled.org/jakelazaroff.com/actor-typeahead\n */\n\nimport { WebComponent } from '@substrate-system/web-component'\n\n// for document.querySelector\ndeclare global {\n interface HTMLElementTagNameMap {\n 'actor-typeahead':ActorTypeahead\n }\n}\n\nexport interface Actor {\n handle:string;\n avatar?:string;\n}\n\nconst DEFAULT_HOST = 'https://public.api.bsky.app'\nconst DEFAULT_ROWS = 5\n\n// The Suggestion list is a top-layer popover, positioned against the host\n// with CSS anchor positioning. `anchor-scope` shipped last of that set, so\n// it gates the group. Without it, the old absolute layout is used.\nconst ANCHORED = typeof CSS !== 'undefined' &&\n CSS.supports('anchor-scope: --a')\n\n// counts instances, so each one gets unique element ids\nlet instances = 0\n\n/**\n * @attribute {string} [host] - The host to which to make the typeahead\n * API call.\n * @attribute {number} [rows] - The maximum number of Suggestions to\n * display in the Suggestion list.\n *\n * @fires actor-typeahead:select - A Suggestion was chosen from the\n * Suggestion list. The event `detail` is `{ handle, avatar }`.\n *\n * @cssprop --color-background - Controls the color of the Suggestion list\n * background.\n * @cssprop --color-border - Controls the color of the Suggestion list\n * border.\n * @cssprop --color-shadow - Controls the color of the Suggestion list\n * shadow.\n * @cssprop --color-hover - Controls the background color of each\n * Suggestion on hover.\n * @cssprop --color-avatar-fallback - Controls the background color of an\n * avatar circle if the image fails to load.\n * @cssprop --radius - Controls the corner radius of the Suggestion list.\n * @cssprop --padding-suggestion-list - Controls the padding of the\n * Suggestion list.\n *\n * @summary A small web component that progressively enhances an <input>\n * element into an autocomplete for ATProto handles.\n *\n * @tag actor-typeahead\n */\nexport class ActorTypeahead extends WebComponent.create('actor-typeahead') {\n static reflectedStringAttributes = ['host', 'rows']\n\n declare host:string|null\n declare rows:string|null\n\n #id = `actor-typeahead-${++instances}`\n #actors:Actor[] = []\n #index = -1\n #pressed = false\n #suggestionList:HTMLUListElement|null = null\n\n constructor () {\n super()\n this.addEventListener('input', this)\n this.addEventListener('focusout', this)\n this.addEventListener('keydown', this)\n }\n\n get #limit ():number {\n const rows = Number.parseInt(this.rows ?? '')\n return Number.isNaN(rows) ? DEFAULT_ROWS : rows\n }\n\n handleEvent (evt:Event) {\n switch (evt.type) {\n case 'input':\n this.#oninput(evt)\n break\n\n case 'keydown':\n if (evt instanceof KeyboardEvent) this.#onkeydown(evt)\n break\n\n case 'focusout':\n this.#onfocusout()\n break\n\n case 'pointerdown':\n this.#pressed = true\n break\n\n case 'pointerup':\n this.#onpointerup(evt)\n break\n }\n }\n\n #onkeydown (evt:KeyboardEvent) {\n const last = this.#actors.length - 1\n\n switch (evt.key) {\n case 'ArrowDown':\n evt.preventDefault()\n this.#index = Math.min(this.#index + 1, last)\n this.render()\n break\n\n case 'PageDown':\n evt.preventDefault()\n this.#index = last\n this.render()\n break\n\n case 'ArrowUp':\n evt.preventDefault()\n this.#index = Math.max(this.#index - 1, 0)\n this.render()\n break\n\n case 'PageUp':\n evt.preventDefault()\n this.#index = 0\n this.render()\n break\n\n case 'Escape':\n evt.preventDefault()\n this.#clear()\n break\n\n case 'Enter':\n evt.preventDefault()\n this.#select(this.#index)\n break\n }\n }\n\n async #oninput (evt:Event) {\n if (!(evt.target instanceof HTMLInputElement)) return\n\n const query = evt.target.value\n if (!query) {\n this.#clear()\n return\n }\n\n const url = new URL(\n 'xrpc/app.bsky.actor.searchActorsTypeahead',\n this.host ?? DEFAULT_HOST\n )\n url.searchParams.set('q', query)\n url.searchParams.set('limit', String(this.#limit))\n\n const res = await fetch(url)\n const json:unknown = await res.json()\n this.#actors = parseActors(json)\n this.#index = -1\n this.render()\n }\n\n #onfocusout () {\n if (this.#pressed) return\n this.#clear()\n }\n\n #onpointerup (evt:Event) {\n this.#pressed = false\n\n const option = evt.target instanceof Element ?\n evt.target.closest<HTMLElement>('[role=\"option\"]') :\n null\n\n if (!option) {\n this.qs('input')?.focus()\n return\n }\n\n this.#select(Number(option.dataset.index))\n }\n\n #select (index:number) {\n const actor = this.#actors[index]\n const input = this.qs('input')\n if (!actor || !input) return\n\n input.value = actor.handle\n input.focus()\n this.#clear()\n this.emit<Actor>('select', { detail: actor })\n }\n\n #clear () {\n this.#actors = []\n this.#index = -1\n this.render()\n }\n\n /**\n * Render into light DOM, next to the authored `<input>`. Only the\n * Suggestion list is rebuilt, so the input element, and anything the\n * consumer attached to it, is never replaced. On the input, only the\n * ARIA combobox attributes are written, plus `autocomplete=\"off\"`\n * when the page did not set `autocomplete`.\n */\n render () {\n if (!this.#suggestionList) {\n this.#suggestionList = document.createElement('ul')\n this.#suggestionList.className = 'suggestion-list'\n this.#suggestionList.id = `${this.#id}-list`\n this.#suggestionList.setAttribute('role', 'listbox')\n if (ANCHORED) this.#suggestionList.popover = 'manual'\n this.#suggestionList.addEventListener('pointerdown', this)\n this.#suggestionList.addEventListener('pointerup', this)\n }\n\n const list = this.#suggestionList\n if (list.parentNode !== this) this.append(list)\n\n list.replaceChildren(...this.#actors.map((actor, i) => {\n const option = suggestion(actor, i, i === this.#index)\n option.id = this.#optionId(i)\n return option\n }))\n\n const open = this.#toggle(list, this.#actors.length > 0)\n\n const input = this.qs('input')\n if (input) {\n input.setAttribute('role', 'combobox')\n input.setAttribute('aria-autocomplete', 'list')\n if (!input.hasAttribute('autocomplete')) {\n input.setAttribute('autocomplete', 'off')\n }\n input.setAttribute('aria-controls', list.id)\n input.setAttribute('aria-expanded', String(open))\n\n const labels = [...(input.labels ?? [])]\n .map(label => label.id)\n .filter(Boolean)\n if (labels.length) {\n list.setAttribute('aria-labelledby', labels.join(' '))\n } else {\n list.removeAttribute('aria-labelledby')\n }\n\n if (this.#actors[this.#index]) {\n input.setAttribute(\n 'aria-activedescendant',\n this.#optionId(this.#index)\n )\n } else {\n input.removeAttribute('aria-activedescendant')\n }\n }\n }\n\n /**\n * Show or hide the Suggestion list popover, guarded on its current\n * state so repeated renders neither throw nor toggle. A disconnected\n * popover cannot be shown, e.g. when a response arrives after the host\n * was removed. Returns whether the Suggestion list is open.\n */\n #toggle (list:HTMLElement, open:boolean):boolean {\n if (!ANCHORED) return open\n if (!list.isConnected) return false\n\n const isOpen = list.matches(':popover-open')\n if (open && !isOpen) list.showPopover()\n if (!open && isOpen) list.hidePopover()\n return list.matches(':popover-open')\n }\n\n #optionId (index:number):string {\n return `${this.#id}-option-${index}`\n }\n}\n\nActorTypeahead.define()\n\n/**\n * Build one Suggestion. Values from the API go through `textContent` and\n * properties, never through `innerHTML`.\n */\nfunction suggestion (\n actor:Actor,\n index:number,\n active:boolean\n):HTMLLIElement {\n const li = document.createElement('li')\n li.className = 'suggestion'\n li.setAttribute('role', 'option')\n li.setAttribute('aria-selected', String(active))\n li.dataset.index = String(index)\n li.dataset.handle = actor.handle\n\n const avatar = document.createElement('div')\n avatar.className = 'avatar'\n\n const img = document.createElement('img')\n img.className = 'img'\n img.alt = ''\n if (actor.avatar) img.src = actor.avatar\n\n const handle = document.createElement('span')\n handle.className = 'handle'\n handle.textContent = actor.handle\n\n avatar.append(img)\n li.append(avatar, handle)\n\n return li\n}\n\nfunction parseActors (json:unknown):Actor[] {\n if (!isRecord(json) || !Array.isArray(json.actors)) return []\n\n return json.actors.flatMap((actor:unknown) => {\n if (!isRecord(actor) || typeof actor.handle !== 'string') return []\n\n return [typeof actor.avatar === 'string' ?\n { handle: actor.handle, avatar: actor.avatar } :\n { handle: actor.handle }]\n })\n}\n\nfunction isRecord (value:unknown):value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright (c) 2026 Jake Lazaroff\n *\n * Repository: https://tangled.org/jakelazaroff.com/actor-typeahead\n */\n\nimport { WebComponent } from '@substrate-system/web-component'\n\n// for document.querySelector\ndeclare global {\n interface HTMLElementTagNameMap {\n 'actor-typeahead':ActorTypeahead\n }\n}\n\nexport interface Actor {\n handle:string;\n avatar?:string;\n}\n\nconst DEFAULT_HOST = 'https://public.api.bsky.app'\nconst DEFAULT_ROWS = 5\n\n// how long the Query must stay unchanged before a Typeahead request\nconst DEBOUNCE_MS = 150\n\n// The Suggestion list is a top-layer popover, positioned against the host\n// with CSS anchor positioning. `anchor-scope` shipped last of that set, so\n// it gates the group. Without it, the old absolute layout is used.\nconst ANCHORED = typeof CSS !== 'undefined' &&\n CSS.supports('anchor-scope: --a')\n\n// counts instances, so each one gets unique element ids\nlet instances = 0\n\n/**\n * @attribute {string} [host] - The host to which to make the typeahead\n * API call.\n * @attribute {number} [rows] - The maximum number of Suggestions to\n * display in the Suggestion list.\n *\n * @fires actor-typeahead:select - A Suggestion was chosen from the\n * Suggestion list. The event `detail` is `{ handle, avatar }`.\n *\n * @cssprop --color-background - Controls the color of the Suggestion list\n * background.\n * @cssprop --color-border - Controls the color of the Suggestion list\n * border.\n * @cssprop --color-shadow - Controls the color of the Suggestion list\n * shadow.\n * @cssprop --color-hover - Controls the background color of each\n * Suggestion on hover.\n * @cssprop --color-avatar-fallback - Controls the background color of an\n * avatar circle if the image fails to load.\n * @cssprop --radius - Controls the corner radius of the Suggestion list.\n * @cssprop --padding-suggestion-list - Controls the padding of the\n * Suggestion list.\n *\n * @summary A small web component that progressively enhances an <input>\n * element into an autocomplete for ATProto handles.\n *\n * @tag actor-typeahead\n */\nexport class ActorTypeahead extends WebComponent.create('actor-typeahead') {\n static reflectedStringAttributes = ['host', 'rows']\n\n declare host:string|null\n declare rows:string|null\n\n /**\n * Returns the text announced when a Typeahead request settles with\n * `count` Suggestions, including 0 when nothing matched the Query.\n * Defaults to English. Replace it to translate; the replacement\n * handles plurals and 0. Read at announce time, so it can be set\n * after connect.\n */\n announce:(count:number) => string = defaultAnnounce\n\n #id = `actor-typeahead-${++instances}`\n #actors:Actor[] = []\n #index = -1\n #suggestionList:HTMLDivElement|null = null\n #suggestionCount:HTMLDivElement|null = null\n #nbsp = false\n #timer:ReturnType<typeof setTimeout>|undefined\n #controller:AbortController|null = null\n\n constructor () {\n super()\n this.addEventListener('input', this)\n this.addEventListener('focusout', this)\n this.addEventListener('keydown', this)\n }\n\n /**\n * Stop any pending Typeahead request. The Suggestion list is left as\n * is; the next Query rebuilds it.\n */\n disconnectedCallback () {\n this.#cancel()\n }\n\n get #limit ():number {\n const rows = Number.parseInt(this.rows ?? '')\n return Number.isNaN(rows) ? DEFAULT_ROWS : rows\n }\n\n handleEvent (evt:Event) {\n switch (evt.type) {\n case 'input':\n this.#oninput(evt)\n break\n\n case 'keydown':\n if (evt instanceof KeyboardEvent) this.#onkeydown(evt)\n break\n\n case 'focusout':\n if (evt instanceof FocusEvent) this.#onfocusout(evt)\n break\n\n case 'mousedown':\n // a press or a tap keeps focus on the Authored input\n evt.preventDefault()\n break\n\n case 'click':\n this.#onclick(evt)\n break\n }\n }\n\n #onkeydown (evt:KeyboardEvent) {\n const last = this.#actors.length - 1\n const moves = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']\n // with no Suggestions there is nothing to move to, and a render\n // would empty a \"No Suggestions\" announcement\n if (last < 0 && moves.includes(evt.key)) {\n evt.preventDefault()\n return\n }\n\n switch (evt.key) {\n case 'ArrowDown':\n evt.preventDefault()\n this.#index = Math.min(this.#index + 1, last)\n this.render()\n break\n\n case 'PageDown':\n evt.preventDefault()\n this.#index = last\n this.render()\n break\n\n case 'ArrowUp':\n evt.preventDefault()\n // wrap from nothing Highlighted to the last Suggestion,\n // and from the first back to nothing (the APG pattern)\n this.#index = this.#index < 0 ? last : this.#index - 1\n this.render()\n break\n\n case 'PageUp':\n evt.preventDefault()\n this.#index = 0\n this.render()\n break\n\n case 'Escape':\n // with the list closed, let a dialog close; still cancel\n // any pending Typeahead request\n if (this.#actors.length) evt.preventDefault()\n this.#clear()\n break\n\n case 'Enter':\n // with nothing Highlighted, let the form submit, and\n // drop any pending Typeahead request\n if (!this.#actors[this.#index]) {\n this.#cancel()\n break\n }\n evt.preventDefault()\n this.#select(this.#index)\n break\n }\n }\n\n #oninput (evt:Event) {\n if (!(evt.target instanceof HTMLInputElement)) return\n\n const query = evt.target.value\n if (!query) {\n this.#clear()\n return\n }\n\n // keep the current Suggestions on screen until the new response\n this.#cancel()\n this.#timer = setTimeout(() => this.#request(query), DEBOUNCE_MS)\n }\n\n async #request (query:string) {\n this.#timer = undefined\n const url = new URL(\n 'xrpc/app.bsky.actor.searchActorsTypeahead',\n this.host ?? DEFAULT_HOST\n )\n url.searchParams.set('q', query)\n url.searchParams.set('limit', String(this.#limit))\n\n const controller = new AbortController()\n this.#controller = controller\n const { signal } = controller\n\n let json:unknown\n try {\n const res = await fetch(url, { signal })\n if (signal.aborted) return\n if (!res.ok) throw new Error(`status ${res.status}`)\n json = await res.json()\n } catch (err) {\n // a newer Query, or a close, cancelled this request\n if (isAbortError(err)) return\n // any other failure closes the Suggestion list, quietly\n this.#clear()\n return\n }\n if (signal.aborted) return\n\n this.#actors = parseActors(json)\n this.#index = -1\n // after render(), which empties the count when there are none\n this.render()\n this.#announce(this.#actors.length)\n }\n\n /**\n * Write the Suggestion count. A trailing no-break space alternates\n * between writes, so screen readers re-read an identical count.\n */\n #announce (count:number) {\n if (!this.#suggestionCount) return\n this.#nbsp = !this.#nbsp\n this.#suggestionCount.textContent = this.announce(count) +\n (this.#nbsp ? '\\u00a0' : '')\n }\n\n /**\n * Close only when focus leaves the host. Some assistive technology\n * focuses a Suggestion before it sends the click that selects it.\n */\n #onfocusout (evt:FocusEvent) {\n const next = evt.relatedTarget\n if (next instanceof Node && this.contains(next)) return\n this.#clear()\n }\n\n #onclick (evt:Event) {\n const option = evt.target instanceof Element ?\n evt.target.closest<HTMLElement>('[role=\"option\"]') :\n null\n if (!option) return\n\n this.#select(Number(option.dataset.index))\n }\n\n #select (index:number) {\n const actor = this.#actors[index]\n const input = this.qs('input')\n if (!actor || !input) return\n\n input.value = actor.handle\n input.focus()\n this.#clear()\n this.emit<Actor>('select', { detail: actor })\n }\n\n /**\n * Stop any pending Typeahead request: the debounce timer, and the\n * request in flight, whose response is then dropped.\n */\n #cancel () {\n clearTimeout(this.#timer)\n this.#timer = undefined\n this.#controller?.abort()\n this.#controller = null\n }\n\n #clear () {\n this.#cancel()\n this.#actors = []\n this.#index = -1\n this.render()\n }\n\n /**\n * Render into light DOM. Only the suggestion list is rebuilt,\n * so the input element, and anything the consumer attached to it,\n * is never replaced. On the input, only the\n * ARIA combobox attributes are written, plus `autocomplete=\"off\"`\n * when the page did not set `autocomplete`.\n */\n render () {\n if (!this.#suggestionList) {\n this.#suggestionList = document.createElement('div')\n this.#suggestionList.className = 'suggestion-list'\n this.#suggestionList.id = `${this.#id}-list`\n this.#suggestionList.setAttribute('role', 'listbox')\n if (ANCHORED) this.#suggestionList.popover = 'manual'\n this.#suggestionList.addEventListener('mousedown', this)\n this.#suggestionList.addEventListener('click', this)\n }\n\n const list = this.#suggestionList\n if (list.parentNode !== this) this.append(list)\n\n // the live region exists empty before its first write, because\n // screen readers often skip a region inserted already filled\n if (!this.#suggestionCount) {\n this.#suggestionCount = document.createElement('div')\n this.#suggestionCount.className = 'suggestion-count'\n this.#suggestionCount.setAttribute('role', 'status')\n }\n const count = this.#suggestionCount\n if (count.parentNode !== this) this.append(count)\n // only a response writes the count; any close empties it, and a\n // response with no Suggestions writes it after this render\n if (!this.#actors.length) count.textContent = ''\n\n list.replaceChildren(...this.#actors.map((actor, i) => {\n const option = suggestion(actor, i, i === this.#index)\n option.id = this.#optionId(i)\n return option\n }))\n\n const open = this.#toggle(list, this.#actors.length > 0)\n\n const input = this.qs('input')\n if (input) {\n input.setAttribute('role', 'combobox')\n input.setAttribute('aria-autocomplete', 'list')\n if (!input.hasAttribute('autocomplete')) {\n input.setAttribute('autocomplete', 'off')\n }\n input.setAttribute('aria-controls', list.id)\n input.setAttribute('aria-expanded', String(open))\n\n // the Suggestion list carries at most one of the two\n const name = nameSource(input, this)\n for (const attr of ['aria-labelledby', 'aria-label'] as const) {\n const value = name[attr]\n if (value) list.setAttribute(attr, value)\n else list.removeAttribute(attr)\n }\n\n if (this.#actors[this.#index]) {\n input.setAttribute(\n 'aria-activedescendant',\n this.#optionId(this.#index)\n )\n } else {\n input.removeAttribute('aria-activedescendant')\n }\n }\n }\n\n /**\n * Show or hide the Suggestion list popover, guarded on its current\n * state so repeated renders neither throw nor toggle. A disconnected\n * popover cannot be shown, e.g. when a response arrives after the host\n * was removed. Returns whether the Suggestion list is open.\n */\n #toggle (list:HTMLElement, open:boolean):boolean {\n if (!ANCHORED) return open\n if (!list.isConnected) return false\n\n const isOpen = list.matches(':popover-open')\n if (open && !isOpen) list.showPopover()\n if (!open && isOpen) list.hidePopover()\n return list.matches(':popover-open')\n }\n\n #optionId (index:number):string {\n return `${this.#id}-option-${index}`\n }\n}\n\nActorTypeahead.define()\n\n/**\n * Build one Suggestion. Values from the API go through `textContent` and\n * properties, never through `innerHTML`.\n */\nfunction suggestion (\n actor:Actor,\n index:number,\n active:boolean\n):HTMLButtonElement {\n const button = document.createElement('button')\n // a button, so every input method can activate it, but not a\n // submit button, and out of the tab order, because focus stays on\n // the Authored input\n button.type = 'button'\n button.tabIndex = -1\n button.className = 'suggestion'\n button.setAttribute('role', 'option')\n button.setAttribute('aria-selected', String(active))\n button.dataset.index = String(index)\n button.dataset.handle = actor.handle\n\n const avatar = document.createElement('div')\n avatar.className = 'avatar'\n\n const img = document.createElement('img')\n img.className = 'img'\n img.alt = ''\n if (actor.avatar) img.src = actor.avatar\n\n const handle = document.createElement('span')\n handle.className = 'handle'\n handle.textContent = actor.handle\n\n avatar.append(img)\n button.append(avatar, handle)\n\n return button\n}\n\n/**\n * What names the Authored input, in the order of accessible name\n * computation, as the attribute the Suggestion list copies it to.\n */\nfunction nameSource (input:HTMLInputElement, host:HTMLElement):{\n 'aria-labelledby'?:string;\n 'aria-label'?:string;\n} {\n const labelledby = input.getAttribute('aria-labelledby')\n if (labelledby) return { 'aria-labelledby': labelledby }\n\n const label = input.getAttribute('aria-label')\n if (label) return { 'aria-label': label }\n\n // reference the labels when they all have ids, otherwise copy their\n // text, because the component never writes ids onto the page\n const labels = [...(input.labels ?? [])]\n if (!labels.length) return {}\n if (labels.every(label => label.id)) {\n return {\n 'aria-labelledby': labels.map(label => label.id).join(' ')\n }\n }\n\n const text = labels\n .map(label => labelText(label, host).trim())\n .filter(Boolean)\n .join(' ')\n return text ? { 'aria-label': text } : {}\n}\n\n/**\n * The text of a label, without the Host element's content, which a\n * label that wraps the Host element would otherwise include.\n */\nfunction labelText (label:HTMLLabelElement, host:HTMLElement):string {\n const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT, {\n acceptNode: node => host.contains(node) ?\n NodeFilter.FILTER_REJECT :\n NodeFilter.FILTER_ACCEPT\n })\n\n let text = ''\n while (walker.nextNode()) text += walker.currentNode.nodeValue\n return text\n}\n\n/**\n * The default English Suggestion count announcement.\n */\nfunction defaultAnnounce (count:number):string {\n if (count === 0) return 'No Suggestions'\n return count === 1 ?\n '1 Suggestion available' :\n `${count} Suggestions available`\n}\n\nfunction parseActors (json:unknown):Actor[] {\n if (!isRecord(json) || !Array.isArray(json.actors)) return []\n\n return json.actors.flatMap((actor:unknown) => {\n if (!isRecord(actor) || typeof actor.handle !== 'string') return []\n\n return [typeof actor.avatar === 'string' ?\n { handle: actor.handle, avatar: actor.avatar } :\n { handle: actor.handle }]\n })\n}\n\nfunction isAbortError (err:unknown):boolean {\n return err instanceof DOMException && err.name === 'AbortError'\n}\n\nfunction isRecord (value:unknown):value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["label"]
7
7
  }
@@ -1 +1 @@
1
- actor-typeahead{--color-background-inherited:var(--color-background,#fff);--color-border-inherited:var(--color-border,#0002);--color-shadow-inherited:var(--color-shadow,#000);--color-hover-inherited:var(--color-hover,#0001);--color-avatar-fallback-inherited:var(--color-avatar-fallback,#0002);--radius-inherited:var(--radius,8px);--padding-suggestion-list-inherited:var(--padding-suggestion-list,4px);anchor-name:--actor-typeahead;anchor-scope:--actor-typeahead;font-family:system-ui;display:block;position:relative;& .suggestion-list,& .suggestion-list *,& .suggestion-list :before,& .suggestion-list :after{box-sizing:border-box;margin:0;padding:0}& .suggestion-list{background-color:var(--color-background-inherited);border:1px solid var(--color-border-inherited);border-radius:var(--radius-inherited);width:100%;box-shadow:0 6px 6px -4px rgb(from var(--color-shadow-inherited) r g b / 20%);padding:var(--padding-suggestion-list-inherited);background-clip:padding-box;flex-direction:column;margin-top:4px;list-style:none;display:flex;position:absolute;left:0;overflow:hidden;&:empty{display:none}&[popover]{position-anchor:--actor-typeahead;position-try-fallbacks:flip-block;inset:auto;top:anchor(bottom);left:anchor(left);margin:0;width:anchor-size(width);color:inherit;margin-block-start:4px;position:fixed;&:not(:popover-open){display:none}}}& .suggestion{all:unset;box-sizing:border-box;border-radius:calc(var(--radius-inherited) - var(--padding-suggestion-list-inherited));cursor:default;align-items:center;gap:8px;width:100%;height:calc(1.5rem + 12px);padding:6px 8px;display:flex;&:hover,&[aria-selected=true]{background-color:var(--color-hover-inherited)}}& .avatar{background-color:var(--color-avatar-fallback-inherited);border-radius:50%;flex-shrink:0;width:1.5rem;height:1.5rem;overflow:hidden}& .img{width:100%;height:100%;display:block}& .handle{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}}actor-typeahead:not(:defined){display:none}
1
+ actor-typeahead{--color-background-inherited:var(--color-background,#fff);--color-border-inherited:var(--color-border,#0007);--color-shadow-inherited:var(--color-shadow,#000);--color-hover-inherited:var(--color-hover,#0001);--color-avatar-fallback-inherited:var(--color-avatar-fallback,#0002);--radius-inherited:var(--radius,8px);--padding-suggestion-list-inherited:var(--padding-suggestion-list,4px);anchor-name:--actor-typeahead;anchor-scope:--actor-typeahead;font-family:system-ui;display:block;position:relative;& .suggestion-list,& .suggestion-list *,& .suggestion-list :before,& .suggestion-list :after{box-sizing:border-box;margin:0;padding:0}& .suggestion-list{background-color:var(--color-background-inherited);border:1px solid var(--color-border-inherited);border-radius:var(--radius-inherited);width:100%;box-shadow:0 6px 6px -4px rgb(from var(--color-shadow-inherited) r g b / 20%);padding:var(--padding-suggestion-list-inherited);background-clip:padding-box;flex-direction:column;margin-top:4px;display:flex;position:absolute;left:0;overflow:hidden;&:empty{display:none}&[popover]{position-anchor:--actor-typeahead;position-try-fallbacks:flip-block;inset:auto;top:anchor(bottom);left:anchor(left);margin:0;width:anchor-size(width);color:inherit;margin-block-start:4px;position:fixed;&:not(:popover-open){display:none}}}& .suggestion-count{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}& .suggestion{all:unset;box-sizing:border-box;border-radius:calc(var(--radius-inherited) - var(--padding-suggestion-list-inherited));cursor:default;align-items:center;gap:8px;width:100%;height:calc(1.5rem + 12px);padding:6px 8px;display:flex;&:hover,&[aria-selected=true]{background-color:var(--color-hover-inherited)}&[aria-selected=true]{outline-offset:-2px;outline:2px solid}@media (forced-colors:active){&[aria-selected=true]{forced-color-adjust:none;color:highlighttext;background-color:highlight}}}& .avatar{background-color:var(--color-avatar-fallback-inherited);border-radius:50%;flex-shrink:0;width:1.5rem;height:1.5rem;overflow:hidden}& .img{width:100%;height:100%;display:block}& .handle{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}}actor-typeahead:not(:defined){display:none}