Skip to main contentSkip to navigation

The finder — a dialog, a field, a row of lists and rows that are objects rather than lines of text. The shell is here; the answer is the caller's.

Search is the one field that reaches everything a surface holds. It is not a picker: a picker fills a field and answers one question — which of these is the one — so it lists what can be chosen and returns a choice. This answers a different one, what is here, and every row leads to the screen that holds it. The two share a dialog exactly once, and the result is a search that opens by asking you to choose something.

Loading...

The shell, and not the answer

What rows there are, in what order, under what heading, and what a press does with one are facts about a venue and its routes. Knowing them is not this component's work, so everything that reads is the caller's state and the rows arrive as children:

<Search
  say="Search tokens, pools and holders"
  open={open}
  onOpen={setOpen}
  query={query}
  onQuery={setQuery}
  lists={lists}
  list={list}
  onList={setList}
>
  <List title="Tokens">
    {found.map((t) => (
      <Result key={t.at} face={<Coins of={[t]} size="list" />} name={t.name} note={t.symbol} at={t.at} go={open(t)} />
    ))}
  </List>
</Search>

What the shell owns is the part that is the same everywhere: the trigger and the keycap on it, the slash from anywhere on the page, the dialog and where the keyboard goes when it closes, the field, the row of words over the lists, and the scroll that keeps the field being typed into on screen.

A row is an object

Every result wears the mark of what it is about, and then says what it is called, what it is tickered, and the key it is filed under. Reading is slower than recognising, and a list of forty strings is forty readings.

What a row does not carry is a figure. A heading names the ordering and that is the whole of what an ordering is for here; a number printed beside each row turns a list somebody is scanning for a name into a column of figures, and a column of figures is a Table.

Result renders a real button, so Tab reaches it, Enter and Space press it, and the focus ring is the browser's. at is drawn abbreviated and read out whole — it is the only field that tells two things sharing three letters apart.

The slash

The key opens the dialog from anywhere on the page, except while somebody is typing: a slash typed into an amount, an address or this dialog's own field is a slash. A modifier is excluded too, which is where a browser's own shortcuts live. The keycap on the trigger states the key, so the shortcut is not hidden.

One finder per page. Two of them answer the same key twice.

What Enter does

enter runs on Enter and only where exactly one row answers. Pasting an identifier leaves one, which is the whole reason it is here — paste, Enter, done, without reaching for a pointer or tabbing past the list to find the row. Where there are several it is null: Enter into an ambiguity has to pick, and picking for somebody is guessing.

Remembering what was reached for

function recall<T>(of: {
  key: string
  most?: number
  whole: (held: unknown) => held is T
  same: (a: T, b: T) => boolean
}): Recall<T>

Two surfaces on one application ask for the same list — a finder and a picker, a search field and a recent-documents menu — and it is one list. A person who typed an identifier into one and then opened the other has reached for it once, not once per dialog, and two stores answer that differently depending on which was opened first. So the store is a value made once and handed to both.

What is kept is where to look and nothing else. A name kept beside a key is a catalogue nobody refreshes: the venue renames the thing and the remembered row goes on saying what it used to be called. Keys are remembered; identity is read again from whatever answered, and a row whose subject the venue no longer holds is not drawn.

whole is asked of every entry on the way in, because storage outlives the code that wrote it — a half-written entry, a value from a build that kept something else, a row naming a venue this build no longer has. Each costs the entry and not the list.

keep takes the current list rather than reading it back, so a store that refuses — private browsing, an embedded frame, a browser set to block site data — costs this visit's memory and nothing more. Reading the property at all throws in those cases, which is why every call is inside a guard.

Props

SearchProps

PropTypeDescription
saystringWhat the field asks for. Worn by the trigger, the dialog's name, and the field's label and placeholder.
openbooleanWhether the dialog is up.
onOpen(next: boolean) => voidThe trigger, the overlay, Escape and the slash all arrive here.
querystringWhat has been typed.
onQuery(next: string) => voidEach keystroke.
lists{ key: string; label: ReactNode }[]The lists on offer. Absent, there is one list and no row of words.
liststringWhich is open.
onList(key: string) => voidChoosing one.
besideReactNodeWhat stands at the end of the field: a venue picker, a readout, nothing.
enter(() => void) | nullWhere Enter goes, and only where exactly one row answers.
widebooleanWhether the trigger carries its words and its keycap, or is the mark alone.
childrenReactNodeThe answer.

Result

PropTypeDescription
faceReactNodeThe mark of whatever the row is about.
namestringWhat it is called.
saidstringWhat it is called when read out, where that is not what is drawn — a row named by an abbreviated identifier identifies nobody.
markstringA word qualifying the name.
notestringWhat it is tickered.
atstringThe key it is filed under, drawn short and read whole.
go() => voidThe press.

List

PropTypeDescription
titleReactNodeThe heading. It comes from the caller's reading: where nothing stated an ordering there is none to name, and the title says so by not claiming one.
actionReactNodeWhat belongs to this run of rows — Clear, and nothing else. No count, no pill.
childrenReactNodeThe rows.

Clear, Lens, short

Clear is the action that empties a remembered list — a word rather than a mark, because it is the only thing on the panel that takes something away and a glyph for that is a glyph a reader has to guess at. Lens is the magnifier, a circle and a stroke. short abbreviates an identifier, keeping six characters of the head and four of the tail, and only where abbreviating shortens: at thirteen characters the abbreviation is the same length, so the middle would be lost for nothing.

What it does not do

It does not read, route, or remember on the caller's behalf. It holds no query state, resolves no identifier, and knows nothing about venues — a caller wanting the dialog to start fresh clears its own query in onOpen. There is no keyboard navigation between rows beyond Tab, because every row is a real button and Tab is what reaches one; there is no highlighting of the matched substring, and no ranking. Which rows answer a query, and in what order, is the caller's, and it is the half of a finder that is about the data.