QuantityInput
A compact count with − and + either side, kept to its limits and step, with the arrow keys, PageUp/PageDown, Home and End.
Demo
Up to 10 per order.
2 tickets, $24 in all.
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/quantity_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/quantity_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/quantity_input.qwe
component QuantityInputDemoLive() { state tickets = 2 <div.demo-stack> <QuantityInput name="qi-demo-tickets" label="Tickets" value={tickets} least={1} most={8} unit="ticket" on_change={(n) => { tickets = n }}/> <p.demo-note>{tickets} {tickets == 1 ? "ticket" : "tickets"}, ${tickets * 12} in all.</p> </div>}view QuantityInputDemo() { <div.demo-stack> <div.demo-row> <QuantityInput name="qi-demo-qty" value={1} least={0} most={10} hint="Up to 10 per order."/> <QuantityInputDemoLive/> </div> <div.demo-row> <QuantityInput name="qi-demo-pack" label="Packs of 6" size="sm" value={6} step={6} least={6} most={48}/> <QuantityInput name="qi-demo-lg" label="Guests" size="lg" value={4}/> <QuantityInput name="qi-demo-off" label="Sold out" value={0} disabled={true}/> </div> </div>}Source
ui/quantity_input.qwe
// QuantityInput: a compact count with − and + on either side, for a cart line,// tickets or guests. The arrow keys step it, PageUp / PageDown by ten steps, Home// and End go to the least and the most; what is typed is kept to the limits and// the step when the box is left. The box is a real <input> named `name`, so a// plain <form> sends the number.//// <QuantityInput name="qty" value={1} least={1} most={10}/>// <QuantityInput name="tickets" label="Tickets" value={2} unit="ticket" on_change={(n) => { tickets = n }}/>//// value: where it starts (1). least: the smallest (0). most: the largest (none).// step: what each press adds (1). unit: what is counted, for screen readers ("3// tickets"; units: its plural, unit + "s" by default). label: shown above it;// without one, screen readers hear "Quantity". size: "sm", "md" (default) or "lg".// name ("quantity"), hint, error, disabled, field (the box's id, "quantity-input-"// + name by default) as for every field. The buttons are disabled at the limits.// on_change(n): inside one of your components, given the number after each change.import ui/theme.qwe use ui_clslet quantity_input_css = css`.ui-quantity-input { display: grid; gap: 6px; min-width: 0; width: fit-content; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-quantity-input-label { font-size: 13px; font-weight: 500; }.ui-quantity-input-box { display: inline-flex; align-items: center; height: 36px; padding: 2px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); transition: border-color .15s, box-shadow .15s;}.ui-quantity-input-box:hover { border-color: var(--ui-strong); }.ui-quantity-input-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-quantity-input-btn { display: grid; place-items: center; flex: none; width: 30px; height: 30px; padding: 0; border: 0; border-radius: 50%; background: var(--ui-subtle); color: var(--ui-fg); cursor: pointer; transition: background .15s, color .15s;}.ui-quantity-input-btn svg { width: 12px; height: 12px; }.ui-quantity-input-btn:hover:not(:disabled) { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-quantity-input-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-quantity-input-btn:disabled { opacity: .4; cursor: not-allowed; }.ui-quantity-input-box input { width: 3.2em; min-width: 0; height: 100%; padding: 0; border: 0; outline: none; background: transparent; text-align: center; color: var(--ui-fg); font: 600 15px var(--ui-font); font-variant-numeric: tabular-nums;}.ui-quantity-input-sm .ui-quantity-input-box { height: 28px; }.ui-quantity-input-sm .ui-quantity-input-btn { width: 22px; height: 22px; }.ui-quantity-input-sm .ui-quantity-input-box input { width: 2.6em; font-size: 13px; }.ui-quantity-input-lg .ui-quantity-input-box { height: 46px; padding: 3px; }.ui-quantity-input-lg .ui-quantity-input-btn { width: 38px; height: 38px; }.ui-quantity-input-lg .ui-quantity-input-box input { width: 3.6em; font-size: 18px; }.ui-quantity-input-invalid .ui-quantity-input-box { border-color: var(--ui-danger); }.ui-quantity-input-disabled { opacity: .55; }.ui-quantity-input-hint, .ui-quantity-input-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-quantity-input-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-quantity-input-box, .ui-quantity-input-btn { transition: none; } }`// n kept between least and most (most may be null), and on a step from least.fn quantity_input_keep(n: int, least: int, most: int?, step: int) -> int { let snapped = least + round((n - least) / step) * step let top = most != null ? least + floor((most - least) / step) * step : null snapped < least ? least : top != null && snapped > top ? top : snapped}component QuantityInput(name: str = "quantity", label: str? = null, value: int = 1, least: int = 0, most: int? = null, step: int = 1, unit: str? = null, units: str? = null, size: str = "md", hint: str? = null, error: str? = null, disabled: bool = false, field: str? = null, on_change = null, class: str? = null) { style global quantity_input_css state n = quantity_input_keep(value, least, most, step) state text = str(quantity_input_keep(value, least, most, step)) let id = field ?? `quantity-input-${name}` let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let said = unit == null ? str(n) : `${n} ${n == 1 ? unit : units ?? unit + "s"}` let set_to = (x: int) => { let k = quantity_input_keep(x, least, most, step) text = str(k) if k != n { n = k on_change && on_change(k) } } <div class={ui_cls(["ui-quantity-input", size != "md" && `ui-quantity-input-${size}`, error != null && "ui-quantity-input-invalid", disabled && "ui-quantity-input-disabled", class])}> {label != null && <label.ui-quantity-input-label for={id}>{label}</label>} <div.ui-quantity-input-box> <button.ui-quantity-input-btn type="button" aria-label={`Decrease${label != null ? ` ${lower(label)}` : ""}`} aria-controls={id} disabled={disabled || n <= least} @click={() => set_to(n - step)}> <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M2 6h8"/></svg> </button> <input type="text" id={id} name={name} value={text} inputmode="numeric" autocomplete="off" disabled={disabled} role="spinbutton" aria-valuenow={n} aria-valuemin={least} aria-valuemax={most} aria-valuetext={said} aria-label={label == null ? "Quantity" : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @input={(e) => { let digits = re.replace(e.value, "[^0-9]", "") text = digits e.target.value = digits }} @change={(e) => { set_to(int(text) ?? n) }} @keydown={(e) => { let to = match e.key { "ArrowUp" => n + step "ArrowDown" => n - step "PageUp" => n + step * 10 "PageDown" => n - step * 10 "Home" => least "End" => most _ => null } if to != null { e.preventDefault() set_to(to) } }}/> <button.ui-quantity-input-btn type="button" aria-label={`Increase${label != null ? ` ${lower(label)}` : ""}`} aria-controls={id} disabled={disabled || (most != null && n + step > most)} @click={() => set_to(n + step)}> <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M2 6h8M6 2v8"/></svg> </button> </div> {hint != null && <p.ui-quantity-input-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-quantity-input-error id={`${id}-error`} role="alert">{error}</p>} </div>}Related
AddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsCurrencyInputAn amount with its currency's symbol, thousands grouped as it is typed and the decimals the currency has, sent as plain digits.5 views · 5 downloadsCartItemOne line of a cart: picture, name and options, a quantity control, the line's total and Remove, as inputs a form sends.6 views · 6 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.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 downloads
Thread
Thread 0 posts
On QuantityInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.