LikertScale
A statement with a row of points from Strongly disagree to Strongly agree: real radios in a fieldset, posted with a form.
Demo
No answer yet.
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/likert_scale.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/likert_scale.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/likert_scale.qwe
component LikertScaleDemoLive() { state answer = null <div.demo-stack> <LikertScale name="recommend" label="I would recommend Quillo to a colleague" points={7} value={answer} field="likert-scale-demo-live" on_change={(v) => { answer = v }}/> <p.demo-note>{answer == null ? "No answer yet." : `You chose ${answer} of 7.`}</p> </div>}view LikertScaleDemo() = <div.demo-stack> <LikertScale name="docs" label="The docs were easy to follow" value={4} field="likert-scale-demo-five" hint="1 is strongly disagree, 5 is strongly agree." required={true}/> <LikertScaleDemoLive/> <LikertScale name="often" label="How often do you deploy?" field="likert-scale-demo-words" points={["Never", "Monthly", "Weekly", "Daily", "Hourly"]} low={null} high={null} error="Pick how often."/> </div>Source
ui/likert_scale.qwe
// LikertScale: one statement and a row of points to agree or disagree with it,// from "Strongly disagree" to "Strongly agree". They are real radio buttons in a// <fieldset>, so a plain <form> sends the point's value (1 to 5) as `name`, and// the arrow keys move between them.//// <LikertScale name="q1" label="The docs were easy to follow"/>// <LikertScale name="q2" label="I would recommend it" points={7} value={5} required={true}/>// <LikertScale name="q3" label="How often?" points={["Never", "Rarely", "Sometimes", "Often", "Always"]}// low={null} high={null} on_change={(v) => { answer = v }}/>//// points: how many (5 by default, or 7; the values are 1 to that), or a list of// the points' own labels, written under each. low and high: the words at the two// ends ("Strongly disagree", "Strongly agree"; null leaves one out). value: the// point chosen to start with (1-based). Each point has a name for screen readers// ("3 of 5", or its own label). hint, error, required, disabled and field (the// ids' start, default "likert-scale-" + name) as in RadioGroup.// on_change(value): inside one of your components, given the point chosen (its// number, 1-based, as a string: what the form sends).import ui/theme.qwe use ui_clslet likert_scale_css = css`.ui-likert-scale { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-likert-scale-legend { padding: 0; margin-bottom: 10px; font-weight: 500; }.ui-likert-scale-req { margin-left: 2px; color: var(--ui-danger); }.ui-likert-scale-row { display: flex; align-items: flex-start; gap: 12px; }.ui-likert-scale-end { flex: 1 1 0; font-size: 13px; color: var(--ui-muted); padding-top: 4px; }.ui-likert-scale-end:last-child { text-align: right; }.ui-likert-scale-points { display: flex; gap: 6px; }.ui-likert-scale-opt { position: relative; display: grid; justify-items: center; gap: 4px; min-width: 36px; cursor: pointer; }.ui-likert-scale-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-likert-scale-dot { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; border: 1.5px solid var(--ui-strong); background: var(--ui-surface); color: var(--ui-muted); font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums; transition: border-color .15s, background .15s, box-shadow .15s;}.ui-likert-scale-opt:hover .ui-likert-scale-dot { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-likert-scale-opt input:checked + .ui-likert-scale-dot { border-color: var(--ui-accent); background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-likert-scale-opt input:focus-visible + .ui-likert-scale-dot { box-shadow: var(--ui-focus); }.ui-likert-scale-word { font-size: 12px; color: var(--ui-muted); text-align: center; max-width: 76px; }.ui-likert-scale-opt input:checked ~ .ui-likert-scale-word { color: var(--ui-fg); font-weight: 500; }.ui-likert-scale-opt:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-likert-scale-invalid .ui-likert-scale-dot { border-color: var(--ui-danger); }.ui-likert-scale-hint, .ui-likert-scale-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-likert-scale-error { color: var(--ui-danger); }@media (max-width: 560px) { .ui-likert-scale-row { flex-wrap: wrap; } .ui-likert-scale-points { order: -1; width: 100%; justify-content: space-between; } .ui-likert-scale-end { flex: 1 1 40%; }}@media (prefers-reduced-motion: reduce) { .ui-likert-scale-dot { transition: none; } }`view LikertScale(name: str, label: str? = null, points: int | list = 5, low: str? = "Strongly disagree", high: str? = "Strongly agree", value: int | str | null = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, on_change = null, class: str? = null) { style global likert_scale_css let id = field ?? `likert-scale-${name}` let words = type_of(points) == "list" ? points | map((p) => str(p)) : null let n = words != null ? len(words) : points let chosen = value != null ? str(value) : null let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) <fieldset class={ui_cls(["ui-likert-scale", error != null && "ui-likert-scale-invalid", class])} id={id} disabled={disabled} role="radiogroup" aria-required={required ? "true" : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}> {label != null && <legend.ui-likert-scale-legend> {label}{required && <span.ui-likert-scale-req aria-hidden="true">*</span>} </legend>} <div.ui-likert-scale-row> {low != null && <span.ui-likert-scale-end aria-hidden="true">{low}</span>} <div.ui-likert-scale-points> {for i in range(n) => { let v = str(i + 1) let word = words != null ? words[i] : null let said = word ?? (i == 0 && low != null ? `${v} of ${n}, ${low}` : i == n - 1 && high != null ? `${v} of ${n}, ${high}` : `${v} of ${n}`) <label.ui-likert-scale-opt for={`${id}-${v}`}> <input type="radio" id={`${id}-${v}`} name={name} value={v} checked={v == chosen} required={required} aria-label={said} @change={(e) => { on_change && on_change(e.value) }}/> <span.ui-likert-scale-dot aria-hidden="true">{v}</span> {word != null && <span.ui-likert-scale-word aria-hidden="true">{word}</span>} </label> }} </div> {high != null && <span.ui-likert-scale-end aria-hidden="true">{high}</span>} </div> {hint != null && <p.ui-likert-scale-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-likert-scale-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
AddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 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 downloadsCheckboxA checkbox drawn by CSS on a real input, with an indeterminate state, and a group of them in a fieldset.5 views · 6 downloadsCheckboxCardsAny number of choices as cards with a title, a line of detail and a check, each a real checkbox a form sends.5 views · 6 downloads
Thread
Thread 0 posts
On LikertScale: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.