SearchInput
A search box with a magnifier and a clear button; Escape clears, Enter submits its form.
Demo
8 of 8: Middlemarch, Moby-Dick, Mrs Dalloway, Persuasion, The Leopard, The Master and Margarita, To the Lighthouse, War and Peace
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/search_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/search_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/search_input.qwe
let search_input_demo_books = ["Middlemarch", "Moby-Dick", "Mrs Dalloway", "Persuasion", "The Leopard", "The Master and Margarita", "To the Lighthouse", "War and Peace"]component SearchInputDemoLive() { state q = "" state sent = "" let found = search_input_demo_books | filter((b) => contains(lower(b), lower(q))) <div.demo-stack.demo-narrow> <SearchInput name="book" label="Find a book" placeholder="Title" on_input={(t) => { q = t }} on_submit={(t) => { sent = t }}/> <p.demo-note>{len(found)} of {len(search_input_demo_books)}: {join(found, ", ")}</p> {sent != "" && <p.demo-note>Enter pressed with “{sent}”.</p>} </div>}view SearchInputDemo() = <div.demo-stack> <form.demo-narrow method="get" action="#search_input"> <SearchInput name="q" placeholder="Search the docs" value="components" hint="Press Enter to search."/> </form> <div.demo-row> <SearchInput name="s" size="sm" placeholder="Small"/> <SearchInput name="d" placeholder="Disabled" disabled={true}/> </div> <SearchInputDemoLive/> </div>Source
ui/search_input.qwe
// SearchInput: a search box with a magnifier, and a button that clears it once// something is typed (Escape clears it too). It is a real <input type="search">,// so in a plain <form method="get"> pressing Enter sends `name` ("q") as it is.//// <form action="/search"><SearchInput placeholder="Search the docs"/></form>// <SearchInput label="Filter" on_input={(text) => { filter = text }}/>//// label: a visible label above it; without one, screen readers call it "Search".// value: the text to start with. hint, error, required, disabled, size ("sm" or// "md"), field (the input's id, "search-input-" + name by default) as for every field.// on_input(text): called with the text as it is typed, and with "" when it is cleared.// on_submit(text): called on Enter; with it, Enter no longer submits the form.// Both work when the SearchInput is inside one of your components.import ui/theme.qwe use ui_clslet search_input_css = css`.ui-search-input { display: grid; gap: 6px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-search-input-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-search-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-search-input-hint { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-search-input-error { margin: 0; font-size: 13px; color: var(--ui-danger); }.ui-search-input-box { position: relative; display: flex; align-items: center; }.ui-search-input-icon { position: absolute; left: 11px; width: 16px; height: 16px; color: var(--ui-muted); pointer-events: none; }.ui-search-input-control { box-sizing: border-box; width: 100%; height: 38px; margin: 0; padding: 0 36px 0 35px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1 var(--ui-font); appearance: none; transition: border-color .15s, box-shadow .15s;}.ui-search-input-control::placeholder { color: var(--ui-muted); opacity: 1; }.ui-search-input-control::-webkit-search-cancel-button, .ui-search-input-control::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }.ui-search-input-control:hover { border-color: var(--ui-strong); }.ui-search-input-control:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-search-input-control[aria-invalid="true"] { border-color: var(--ui-danger); }.ui-search-input-control:disabled { opacity: .55; cursor: not-allowed; }.ui-search-input-clear { position: absolute; right: 5px; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-search-input-clear svg { width: 14px; height: 14px; }.ui-search-input-clear:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-search-input-clear:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-search-input-sm .ui-search-input-control { height: 30px; padding-left: 31px; font-size: 13px; }.ui-search-input-sm .ui-search-input-icon { left: 9px; width: 14px; height: 14px; }.ui-search-input-sm .ui-search-input-clear { right: 3px; width: 24px; height: 24px; }@media (prefers-reduced-motion: reduce) { .ui-search-input-control { transition: none; } }`component SearchInput(name: str = "q", label: str? = null, value: str = "", placeholder: str? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_input = null, on_submit = null, class: str? = null) { style global search_input_css state text = value let fid = field ?? `search-input-${name}` let described = ui_cls([hint != null && `${fid}-hint`, error != null && `${fid}-error`]) let clear = (e) => { e.target.closest(".ui-search-input-box").querySelector("input").focus() text = "" on_input && on_input("") } <div class={ui_cls(["ui-search-input", size == "sm" && "ui-search-input-sm", class])}> {label != null && <label.ui-search-input-label for={fid}> {label}{required && <span.ui-search-input-req aria-hidden="true">*</span>} </label>} <div.ui-search-input-box> <svg.ui-search-input-icon viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.75"/><path d="M10.5 10.5L14 14"/></svg> <input.ui-search-input-control type="search" id={fid} name={name} value={text} placeholder={placeholder} aria-label={label == null ? "Search" : null} required={required} disabled={disabled} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} autocomplete="off" @input={(e) => { text = e.value on_input && on_input(text) }} @keydown={(e) => { if e.key == "Escape" && text != "" { e.preventDefault() text = "" on_input && on_input("") } if e.key == "Enter" && on_submit != null { e.preventDefault() on_submit(text) } }}/> {text != "" && !disabled && <button.ui-search-input-clear type="button" aria-label="Clear search" @click={clear}> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg> </button>} </div> {hint != null && <p.ui-search-input-hint id={`${fid}-hint`}>{hint}</p>} {error != null && <p.ui-search-input-error id={`${fid}-error`} role="alert">{error}</p>} </div>}Related
AutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 downloadsComboboxA text box that filters a list of options as you type, or asks for them (a server, a map service), with the arrow keys, Enter and Escape.5 views · 6 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloadsCharacterCounterA text box or textarea with a live 120 / 280 count that warns near the limit, as a hard stop or a soft one that blocks sending.5 views · 6 downloadsCheckboxA checkbox drawn by CSS on a real input, with an indeterminate state, and a group of them in a fieldset.5 views · 6 downloads
Thread
Thread 0 posts
On SearchInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.