diff --git a/.changeset/bright-moles-explode.md b/.changeset/bright-moles-explode.md new file mode 100644 index 0000000..e362754 --- /dev/null +++ b/.changeset/bright-moles-explode.md @@ -0,0 +1,5 @@ +--- +"web-haptics": patch +--- + +Fix `WebHaptics.isSupported` so it treats touch fallback devices like iPhone as supported, and add `WebHaptics.supportsVibrationApi` for callers that need the narrower `navigator.vibrate` signal. diff --git a/packages/web-haptics/README.md b/packages/web-haptics/README.md index 73c292a..0f7e49b 100644 --- a/packages/web-haptics/README.md +++ b/packages/web-haptics/README.md @@ -112,7 +112,13 @@ Show or hide the haptic feedback toggle switch. ### `WebHaptics.isSupported` -Static boolean — `true` if the device supports the Vibration API. +Static boolean — `true` if WebHaptics can attempt haptics on this device, +either through the Vibration API or the touch fallback used on platforms like +iPhone. + +### `WebHaptics.supportsVibrationApi` + +Static boolean — `true` if the device exposes `navigator.vibrate`. ## License diff --git a/packages/web-haptics/src/lib/web-haptics/index.ts b/packages/web-haptics/src/lib/web-haptics/index.ts index e54ca9e..e3eccf2 100644 --- a/packages/web-haptics/src/lib/web-haptics/index.ts +++ b/packages/web-haptics/src/lib/web-haptics/index.ts @@ -133,6 +133,39 @@ function toVibratePattern( let instanceCounter = 0; +function supportsVibrationApi(): boolean { + return ( + typeof navigator !== "undefined" && typeof navigator.vibrate === "function" + ); +} + +function supportsTouchFallback(): boolean { + if (typeof navigator === "undefined") { + return false; + } + + if (navigator.maxTouchPoints > 0) { + return true; + } + + if (typeof window === "undefined") { + return false; + } + + if ("ontouchstart" in window) { + return true; + } + + if (typeof window.matchMedia !== "function") { + return false; + } + + return ( + window.matchMedia("(pointer: coarse)").matches || + window.matchMedia("(any-pointer: coarse)").matches + ); +} + export class WebHaptics { private hapticLabel: HTMLLabelElement | null = null; private domInitialized = false; @@ -152,8 +185,13 @@ export class WebHaptics { this.showSwitch = options?.showSwitch ?? false; } - static readonly isSupported: boolean = - typeof navigator !== "undefined" && typeof navigator.vibrate === "function"; + static get supportsVibrationApi(): boolean { + return supportsVibrationApi(); + } + + static get isSupported(): boolean { + return supportsVibrationApi() || supportsTouchFallback(); + } async trigger( input: HapticInput = [{ duration: 25, intensity: 0.7 }], diff --git a/packages/web-haptics/src/react/useWebHaptics.ts b/packages/web-haptics/src/react/useWebHaptics.ts index ac0ce42..2fa152d 100644 --- a/packages/web-haptics/src/react/useWebHaptics.ts +++ b/packages/web-haptics/src/react/useWebHaptics.ts @@ -36,6 +36,7 @@ export function useWebHaptics(options?: WebHapticsOptions) { const cancel = useCallback(() => instanceRef.current?.cancel(), []); const isSupported = WebHaptics.isSupported; + const supportsVibrationApi = WebHaptics.supportsVibrationApi; - return { trigger, cancel, isSupported }; + return { trigger, cancel, isSupported, supportsVibrationApi }; } diff --git a/packages/web-haptics/src/svelte/createWebHaptics.ts b/packages/web-haptics/src/svelte/createWebHaptics.ts index db51ac9..f67183a 100644 --- a/packages/web-haptics/src/svelte/createWebHaptics.ts +++ b/packages/web-haptics/src/svelte/createWebHaptics.ts @@ -14,6 +14,14 @@ export function createWebHaptics(options?: WebHapticsOptions) { const destroy = () => instance.destroy(); const setDebug = (debug: boolean) => instance.setDebug(debug); const isSupported = WebHaptics.isSupported; + const supportsVibrationApi = WebHaptics.supportsVibrationApi; - return { trigger, cancel, destroy, setDebug, isSupported }; + return { + trigger, + cancel, + destroy, + setDebug, + isSupported, + supportsVibrationApi, + }; } diff --git a/packages/web-haptics/src/vue/useWebHaptics.ts b/packages/web-haptics/src/vue/useWebHaptics.ts index 92c9ab7..7c690db 100644 --- a/packages/web-haptics/src/vue/useWebHaptics.ts +++ b/packages/web-haptics/src/vue/useWebHaptics.ts @@ -29,6 +29,7 @@ export function useWebHaptics(options?: WebHapticsOptions) { instance?.trigger(input, options); const cancel = () => instance?.cancel(); const isSupported = WebHaptics.isSupported; + const supportsVibrationApi = WebHaptics.supportsVibrationApi; - return { trigger, cancel, isSupported }; + return { trigger, cancel, isSupported, supportsVibrationApi }; } diff --git a/site/src/surfaces/docs/index.tsx b/site/src/surfaces/docs/index.tsx index 59f0731..138c946 100644 --- a/site/src/surfaces/docs/index.tsx +++ b/site/src/surfaces/docs/index.tsx @@ -61,7 +61,12 @@ const methods = [ { signature: "WebHaptics.isSupported: boolean", description: - "Static property. Returns true if the device supports the Vibration API.", + "Static property. Returns true if WebHaptics can attempt haptics through either the Vibration API or the touch fallback.", + }, + { + signature: "WebHaptics.supportsVibrationApi: boolean", + description: + "Static property. Returns true when the device exposes navigator.vibrate.", }, ]; @@ -175,7 +180,7 @@ export const Docs = () => { @@ -186,7 +191,7 @@ const { trigger, cancel, isSupported } = useWebHaptics({ @@ -195,7 +200,7 @@ const { trigger, cancel, isSupported } = useWebHaptics();`}