From 0799e1593b9ad6e15dbdf0374d1c21aefcb2c95b Mon Sep 17 00:00:00 2001 From: Sergey Korolev Date: Sat, 25 Jul 2026 22:50:44 +0300 Subject: [PATCH] Document the haptic component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No video on this one, and the page says why: haptics are invisible and simulators do not vibrate, so a recording would show three taps and a line of text while the thing being documented stays off-screen. Instead the page ends with the three reasons you might feel nothing, ordered by how often they are the actual cause — the environment is far more likely than the wiring. --- docs/.vitepress/config.mts | 1 + docs/components/haptic.md | 155 +++++++++++++++++++++++++++++++ vendor/hotwire-bridge-components | 2 +- 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 docs/components/haptic.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 7e3d609..4764dd4 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -37,6 +37,7 @@ export default defineConfig({ { text: 'Overview', link: '/components/overview' }, { text: 'Alert', link: '/components/alert' }, { text: 'Button', link: '/components/button' }, + { text: 'Haptic', link: '/components/haptic' }, ], }, { diff --git a/docs/components/haptic.md b/docs/components/haptic.md new file mode 100644 index 0000000..4ff1399 --- /dev/null +++ b/docs/components/haptic.md @@ -0,0 +1,155 @@ +# Haptic + +**Native haptic feedback** — a tap you feel rather than see. The web side asks +for a feedback type; iOS plays it through `UINotificationFeedbackGenerator` and +Android through `View.performHapticFeedback`. + +Outside of Hotwire Native the hook falls back to `navigator.vibrate` where the +browser has it, and does nothing where it does not. + +::: tip No video on this page +Every other component page shows a screen recording. This one cannot — haptics +are invisible, and simulators and emulators do not vibrate at all. Run the demo +app on a real device to feel it. +::: + +::: tip You copy it, you own it +There is nothing to install. The files below are the complete component — paste +the web one plus whichever platforms you ship into your app, and change them +however you like. They are shown straight from the +[hotwire-bridge-components](https://github.com/zumkorn/hotwire-bridge-components/tree/main/registry/haptic) +registry, so what you see here is what the registry holds. +::: + +## Web side + +Save this as `bridge/useBridgeHaptic.tsx` in your app: + +::: code-group + +<<< @/../vendor/hotwire-bridge-components/registry/haptic/inertia/react.tsx [useBridgeHaptic.tsx] + +::: + +Then call `vibrate` wherever something worth feeling happens: + +```jsx +import { useBridgeHaptic } from '@/bridge/useBridgeHaptic' + +function SaveButton({ onSave }) { + const { vibrate } = useBridgeHaptic() + + const save = async () => { + try { + await onSave() + vibrate('success') + } catch { + vibrate('error') + } + } + + return +} +``` + +| Argument | Type | Default | Purpose | +| --- | --- | --- | --- | +| `feedback` | `'success' \| 'warning' \| 'error'` | `'success'` | Which feedback to play | + +`useBridgeHaptic()` also returns `supported`, for a page that wants to hide a +control that would do nothing. + +::: tip The cheapest component here +Native never replies, so no callback is ever registered and there is nothing to +clean up — unlike [Alert](/components/alert) and [Button](/components/button), +where a reply arrives and the callback has to be managed. If you write your own +web side for this one, `send` and forget. +::: + +## iOS side + +Add this file to your Xcode project: + +::: code-group + +<<< @/../vendor/hotwire-bridge-components/registry/haptic/native/HapticComponent.swift [HapticComponent.swift] + +::: + +Then register it at launch, in `AppDelegate`: + +```swift +Hotwire.registerBridgeComponents([ + HapticComponent.self, + // … your other components +]) +``` + +## The contract + +Component name: `haptic`. + +### `vibrate` — web → native + +Plays one piece of feedback. Fire-and-forget. + +```jsonc +{ + "feedback": "success" // "success" | "warning" | "error", optional +} +``` + +There is no reply. Nothing comes back because nothing needs to — the feedback +either played or the device cannot play it, and neither changes what the web +side does next. + +::: warning An unknown type still plays +Native must not drop a `feedback` it does not recognise; it plays `success` +instead. That way a page built against a newer contract keeps working against an +older app, which is the same additive rule every component here follows. +::: + +`feedback` is a *category*, not a waveform. What each one feels like is the +native side's choice and differs between platforms — do not build web-side logic +that assumes a duration or an intensity. + +## Android + +The Kotlin half uses `View.performHapticFeedback`, so it needs no `VIBRATE` +permission and respects the device's own haptic settings. Add this file to your +project: + +::: code-group + +<<< @/../vendor/hotwire-bridge-components/registry/haptic/native/HapticComponent.kt [HapticComponent.kt] + +::: + +Then register it at launch, in your `Application`: + +```kotlin +Hotwire.registerBridgeComponents( + BridgeComponentFactory("haptic", ::HapticComponent), + // … your other components +) +``` + +::: warning Three types, two constants +Android has no direct equivalent of iOS's success/warning/error triple. +`CONFIRM` and `REJECT` cover success and error from API 30 onwards; below that, +and for `warning` on every version, the component falls back to constants that +have always existed. Expect the three to feel less distinct than they do on iOS. +::: + +## Why you might feel nothing + +In rough order of likelihood: + +- **A simulator or emulator.** Neither vibrates, ever. +- **System haptics are off**, or the iPhone is in Low Power Mode, which + suppresses them. +- **The native half is not registered** — then `supported` is false on the web + side and the call goes to `navigator.vibrate`, which desktop and iOS Safari do + not have. + +Log the message on the native side to tell the first two apart from the third. diff --git a/vendor/hotwire-bridge-components b/vendor/hotwire-bridge-components index 8b6d14c..ce7c706 160000 --- a/vendor/hotwire-bridge-components +++ b/vendor/hotwire-bridge-components @@ -1 +1 @@ -Subproject commit 8b6d14cebadc721efc3400795c08583c2fd89eff +Subproject commit ce7c7065d993ce25ab67999f7432ce877df4871a