diff --git a/README.md b/README.md index 3d7ce486..bda7a8bd 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ React Native Sortables is a powerful and easy-to-use library that brings smooth, - **Auto-scrolling** beyond screen bounds - Customizable **layout animations** for items addition and removal - - Built-in **haptic feedback** integration (requires [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) dependency) + - Built-in **haptic feedback** integration via [expo-haptics](https://docs.expo.dev/versions/latest/sdk/haptics/) or [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) - Different **reordering strategies** (insertion, swapping) - 💡 **Developer Experience** diff --git a/packages/docs/docs/flex/props.mdx b/packages/docs/docs/flex/props.mdx index 6ebe0ae0..d2b6f43b 100644 --- a/packages/docs/docs/flex/props.mdx +++ b/packages/docs/docs/flex/props.mdx @@ -899,7 +899,7 @@ Whether haptics are enabled. Vibrations are fired when the **pressed item become :::important -To use built-in haptics, you have to install `react-native-haptic-feedback` package. See this [Getting Started](../getting-started#optional-dependencies) section for more details. +To use built-in haptics, install `expo-haptics` (Expo apps, including Expo Go) or `react-native-haptic-feedback` (bare React Native). The library auto-detects whichever is available. See this [Getting Started](../getting-started#optional-dependencies) section for more details. You can also use any other haptics library but you will have to trigger haptics manually when callbacks are called. See the [Callbacks](#callbacks) section for more details. diff --git a/packages/docs/docs/getting-started.mdx b/packages/docs/docs/getting-started.mdx index 756158a9..28cdd1c2 100644 --- a/packages/docs/docs/getting-started.mdx +++ b/packages/docs/docs/getting-started.mdx @@ -19,8 +19,10 @@ Before getting started, you need to install and configure the following dependen ### Optional Dependencies -- **react-native-haptic-feedback**: For haptic feedback support - - Follow the installation guide in the [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) README +Haptic feedback is optional. The library automatically detects which haptics package is available, preferring **expo-haptics**: + +- **expo-haptics**: recommended for Expo apps (already bundled in Expo Go). Install with `npx expo install expo-haptics` +- **react-native-haptic-feedback**: for bare React Native apps. Follow the installation guide in its [README](https://github.com/mkuczera/react-native-haptic-feedback) ## 2. Installation diff --git a/packages/docs/docs/grid/props.mdx b/packages/docs/docs/grid/props.mdx index 2fb884dd..525d6c7c 100644 --- a/packages/docs/docs/grid/props.mdx +++ b/packages/docs/docs/grid/props.mdx @@ -785,7 +785,7 @@ Whether haptics are enabled. Vibrations are fired when the **pressed item become :::important -To use built-in haptics, you have to install `react-native-haptic-feedback` package. See this [Getting Started](../getting-started#optional-dependencies) section for more details. +To use built-in haptics, install `expo-haptics` (Expo apps, including Expo Go) or `react-native-haptic-feedback` (bare React Native). The library auto-detects whichever is available. See this [Getting Started](../getting-started#optional-dependencies) section for more details. You can also use any other haptics library but you will have to trigger haptics manually when callbacks are called. See the [Callbacks](#callbacks) section for more details. diff --git a/packages/react-native-sortables/src/integrations/haptics/adapters/expo-haptics.ts b/packages/react-native-sortables/src/integrations/haptics/adapters/expo-haptics.ts new file mode 100644 index 00000000..5288e0be --- /dev/null +++ b/packages/react-native-sortables/src/integrations/haptics/adapters/expo-haptics.ts @@ -0,0 +1,49 @@ +/** + * Optional expo-haptics adapter. + * + * We never import `expo-haptics` directly, because a static import would break + * Metro bundling for bare React Native apps that don't have it installed. + * Instead we read its native module from the Expo runtime registry, so it's + * picked up automatically in any Expo app (including Expo Go) and ignored + * everywhere else. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { runOnJS } from 'react-native-reanimated'; + +const load = () => { + const expoHaptics = (globalThis as any).expo?.modules?.ExpoHaptics; + + if (!expoHaptics?.impactAsync) { + return null; + } + + const impact = (style: string) => { + try { + // expo-haptics' native method is async; fire-and-forget and swallow + // rejections (e.g. when haptics are unsupported on the device) + const result = expoHaptics.impactAsync(style); + result?.catch?.(() => { + // ignore rejection + }); + } catch { + // ignore + } + }; + + const trigger = (type = 'impactLight') => { + 'worklet'; + // expo-haptics runs on the JS thread, so hop over from the UI worklet + runOnJS(impact)(type === 'impactMedium' ? 'medium' : 'light'); + }; + + return trigger; +}; + +const ExpoHaptics = { load }; + +export default ExpoHaptics; diff --git a/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts b/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts index df59ef65..e60bc4ab 100644 --- a/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts +++ b/packages/react-native-sortables/src/integrations/haptics/adapters/index.native.ts @@ -1 +1,8 @@ -export { default as ReactNativeHapticFeedback } from './react-native-haptic-feedback'; +import ExpoHaptics from './expo-haptics'; +import ReactNativeHapticFeedback from './react-native-haptic-feedback'; + +export const Haptics = { + // Prefer expo-haptics (available in any Expo app, including Expo Go) and + // fall back to react-native-haptic-feedback for bare React Native apps. + load: () => ExpoHaptics.load() ?? ReactNativeHapticFeedback.load() +}; diff --git a/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts b/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts index f039c3b9..2f367f58 100644 --- a/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts +++ b/packages/react-native-sortables/src/integrations/haptics/adapters/index.ts @@ -1,7 +1,5 @@ -import type { HapticOptions } from 'react-native-haptic-feedback'; - -export const ReactNativeHapticFeedback = { - load: () => (_type: string, _options?: HapticOptions) => { - // noop +export const Haptics = { + load: () => (_type?: string) => { + // noop on web } }; diff --git a/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts b/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts index 61aa64e2..a90cabdd 100644 --- a/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts +++ b/packages/react-native-sortables/src/integrations/haptics/hooks/useHaptics.ts @@ -2,22 +2,21 @@ import { useCallback, useMemo } from 'react'; import { useDerivedValue } from 'react-native-reanimated'; import { IS_WEB } from '../../../constants'; -import { ReactNativeHapticFeedback } from '../adapters'; +import { Haptics } from '../adapters'; type HapticImpact = { light(): void; medium(): void; }; -let hapticFeedback: null | ReturnType = - null; +let hapticFeedback: null | ReturnType = null; export default function useHaptics(enabled: boolean): HapticImpact { const isEnabled = !IS_WEB && enabled; const enabledValue = useDerivedValue(() => isEnabled); if (isEnabled && !hapticFeedback) { - hapticFeedback = ReactNativeHapticFeedback.load(); + hapticFeedback = Haptics.load(); } const light = useCallback(() => {