Mobile

HapticButton

A button with the phone's own feel, falling back to vibrate and then to nothing, with nine named patterns.

#mobile#interactive#haptics#vibrate#button

Demo

Preview

Pressed 0 times; the last was felt: nothing yet.

Get it

It needs theme.qwe, as every component does, and button.qwe; 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/button.qwe$ curl -O https://quillolang.org/components/ui/haptic_button.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/haptic_button.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/haptic_button.qwe
let haptic_button_demo_css = css`.ui-hapbtn-demo { display: grid; grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); gap: 10px; }`component HapticButtonDemoLive() {    style global haptic_button_demo_css    state presses = 0    state last = "nothing yet"    state quiet = true    let note = (how) => { last = how }    <div.demo-stack>        <div.ui-hapbtn-demo>            {for n in ["selection", "light", "medium", "heavy", "soft", "rigid"] => <HapticButton key={n} pattern={n} variant="soft" full={true} say={false}                on_click={() => { presses += 1 }} on_feel={note}>{upper(slice(n, 0, 1)) + slice(n, 1, len(n))}</HapticButton>}        </div>        <div.ui-hapbtn-demo>            <HapticButton pattern="success" full={true} on_click={() => { presses += 1 }} on_feel={note}>Success</HapticButton>            <HapticButton pattern="warning" variant="outline" full={true} on_click={() => { presses += 1 }} on_feel={note}>Warning</HapticButton>            <HapticButton pattern="error" tone="danger" full={true} on_click={() => { presses += 1 }} on_feel={note}>Error</HapticButton>        </div>        <div.demo-row>            <HapticButton pattern="thud" variant="outline" say={true} on_click={() => { presses += 1 }} on_feel={note}>A name it does not know</HapticButton>            <HapticButton muted={quiet} variant="ghost" say={true} on_click={() => { presses += 1 }} on_feel={note}>Muted</HapticButton>            <HapticButton disabled={true} on_click={() => { presses += 1 }}>Disabled</HapticButton>            <Button size="lg" variant="ghost" id="haptic-button-demo-quiet" on_click={() => { quiet = !quiet }}>{quiet ? "Unmute" : "Mute"}</Button>        </div>        <p.demo-note id="haptic-button-demo-note">Pressed {presses} times; the last was felt: {last}.</p>    </div>}view HapticButtonDemo() = <HapticButtonDemoLive/>

Source

ui/haptic_button.qwe
// HapticButton: a button that is felt as well as pressed. In the Quillo app it uses the// phone's own haptics (quilloNative); in a browser with `navigator.vibrate`, Android's, it// plays a short pattern of the same name; elsewhere, iOS Safari among them, it is an// ordinary button and nothing is missing but the feel. Each pattern has a name, the same// nine everywhere, and the button says which way it was felt, for a test or a settings page.////   <HapticButton pattern="success" on_click={() => { save() }}>Save</HapticButton>//   <HapticButton pattern="error" tone="danger" say={true}>Delete</HapticButton>//// pattern: "light", "medium" (the default, and what a name it does not know means),// "heavy", "rigid", "soft", "selection", "success", "warning" or "error".// muted: no feel at all, for a person who turned it off in the app's settings (false).// say: shows under the button how the last press was felt (false).// on_click(): inside one of your components, what a press does. It is called whether or// not anything could be felt. on_feel(how): called after it with "app", "vibrate", "none"// or "muted". The wrapper has data-pattern and data-felt, the same words, for a test.// variant, tone, full, disabled, loading, type, name, value, label, id: as for Button. There is no small size:// a thing to feel is a thing for a thumb.import ui/theme.qwe use ui_clsimport ui/button.qwe use Buttonlet haptic_button_css = css`.ui-hapbtn { display: inline-grid; gap: 4px; justify-items: stretch; font: 13px/1.4 var(--ui-font); }.ui-hapbtn-full { display: grid; }.ui-hapbtn-say { color: var(--ui-muted); text-align: center; }.ui-hapbtn .ui-btn { touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: transform .08s; }.ui-hapbtn .ui-btn:active { transform: scale(.97); }@media (prefers-reduced-motion: reduce) { .ui-hapbtn .ui-btn:active { transform: none; } }`// The nine names, in the order a settings page would list them.let haptic_button_names = ["selection", "light", "medium", "heavy", "soft", "rigid", "success", "warning", "error"]// A name the app knows; anything else is medium, as the apps themselves take it.fn haptic_button_kind(name) = haptic_button_names | contains(name) ? name : "medium"// The same names as vibrate patterns, in milliseconds on, off, on. A motor takes// about 10 ms to be felt at all. The three that report an outcome have two or more// beats, as the system's do, so that they can be told apart with the phone in a hand.fn haptic_button_pattern(name) = match haptic_button_kind(name) {    "selection" => [8]    "light" => [12]    "heavy" => [40]    "soft" => [30]    "rigid" => [16]    "success" => [15, 60, 30]    "warning" => [30, 80, 30]    "error" => [50, 60, 50, 60, 50]    _ => [20]}// What a press's route was, in words.fn haptic_button_words(how, kind) = match how {    "app" => `Felt ${kind} through the app`    "vibrate" => `Vibrated ${kind}: ${join(haptic_button_pattern(kind) | map((n) => str(n)), ", ")} ms`    "none" => "This device has no way to feel it"    "muted" => "Turned off"    _ => ""}component HapticButton(pattern: str = "medium", muted: bool = false, say: bool = false, variant: str = "solid", tone: str = "accent",                       full: bool = false, disabled: bool = false, loading: bool = false, type: str = "button", name: str? = null,                       value: str? = null, label: str? = null, id: str? = null, on_click = null, on_feel = null,                       class: str? = null, children) {    style global haptic_button_css    state felt = null    let kind = haptic_button_kind(pattern)    // The app first. Its call failing is no reason to do without: vibrate may still be there.    let feel = () => {        let r = muted ? null : js("quilloNative.haptic", kind)        if muted {            "muted"        } else if !is_err(r) {            "app"        } else {            // false is a refusal: no recent tap, or vibration turned off in the system.            js("navigator.vibrate", haptic_button_pattern(kind)) == true ? "vibrate" : "none"        }    }    <span class={ui_cls(["ui-hapbtn", full && "ui-hapbtn-full", class])} data-pattern={kind} data-felt={felt}>        <Button size="lg" variant={variant} tone={tone} full={full} disabled={disabled} loading={loading} type={type} name={name}                value={value} label={label} id={id} on_click={() => {                    on_click && on_click()                    felt = feel()                    on_feel && on_feel(felt)                }}>{children}</Button>        {say && <span.ui-hapbtn-say role="status">{haptic_button_words(felt, kind)}</span>}    </span>}
Thread
26Useful? Like it, or add to its thread below.

Thread 0 posts

On HapticButton: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.