Skip to content

Commit 7e85bb3

Browse files
committed
feat: add haptic feedback support to ButtonAnimated component
1 parent 6e05f97 commit 7e85bb3

11 files changed

Lines changed: 83 additions & 4 deletions

File tree

packages/design-system-react-native/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ module.exports = merge(baseConfig, {
4343
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
4444
},
4545
transformIgnorePatterns: [
46-
'node_modules/(?!react-native|@react-native|react-native-reanimated|@react-navigation)',
46+
'node_modules/(?!react-native|@react-native|react-native-reanimated|react-native-nitro-haptics|react-native-nitro-modules|@react-navigation)',
4747
],
4848
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
4949
moduleNameMapper: {

packages/design-system-react-native/jest.setup.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ jest.mock('react-native-svg', () => {
1515
};
1616
});
1717

18+
jest.mock('react-native-nitro-haptics', () => ({
19+
Haptics: {
20+
impact: jest.fn(),
21+
notification: jest.fn(),
22+
selection: jest.fn(),
23+
},
24+
}), { virtual: true });
25+
1826
jest.mock('react-native-reanimated', () => {
1927
const Reanimated = require('react-native-reanimated/mock');
2028

packages/design-system-react-native/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696
"react": ">=18.2.0",
9797
"react-native": ">=0.72.0",
9898
"react-native-gesture-handler": ">=1.10.3",
99+
"react-native-nitro-haptics": ">=0.3.0",
100+
"react-native-nitro-modules": ">=0.25.0",
99101
"react-native-reanimated": ">=3.3.0",
100102
"react-native-safe-area-context": ">=4.0.0"
101103
},

packages/design-system-react-native/src/components/ButtonBase/ButtonBase.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const ButtonBase = ({
3636
accessibilityRole = 'button',
3737
accessibilityActions,
3838
onAccessibilityAction,
39+
hapticFeedback,
3940
...props
4041
}: ButtonBaseProps) => {
4142
const tw = useTailwind();
@@ -93,6 +94,7 @@ export const ButtonBase = ({
9394
return (
9495
<ButtonAnimated
9596
disabled={isDisabled || isLoading}
97+
hapticFeedback={hapticFeedback}
9698
accessibilityRole={accessibilityRole}
9799
accessibilityLabel={finalAccessibilityLabel}
98100
accessibilityHint={finalAccessibilityHint}

packages/design-system-react-native/src/components/ButtonBase/ButtonBase.types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { PressableProps, StyleProp, ViewStyle } from 'react-native';
22

33
import type { ButtonBaseSize } from '../../types';
44
import type { IconProps, IconName } from '../Icon';
5+
import type { HapticFeedbackStyle } from '../temp-components/ButtonAnimated';
56
import type { SpinnerProps } from '../temp-components/Spinner';
67
import type { TextProps } from '../Text';
78

@@ -120,6 +121,12 @@ export type ButtonBaseProps = {
120121
onAccessibilityAction?: (event: {
121122
nativeEvent: { actionName: string };
122123
}) => void;
124+
/**
125+
* Optional haptic feedback style triggered on press.
126+
*
127+
* @default 'light'
128+
*/
129+
hapticFeedback?: HapticFeedbackStyle;
123130
} & Omit<
124131
PressableProps,
125132
| 'accessibilityRole'

packages/design-system-react-native/src/components/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ export { Card } from './Card';
101101
export type { CardProps } from './Card';
102102

103103
export { ButtonAnimated } from './temp-components/ButtonAnimated';
104-
export type { ButtonAnimatedProps } from './temp-components/ButtonAnimated';
104+
export type {
105+
ButtonAnimatedProps,
106+
HapticFeedbackStyle,
107+
} from './temp-components/ButtonAnimated';
105108

106109
export { ButtonBase, ButtonBaseSize } from './ButtonBase';
107110
export type { ButtonBaseProps } from './ButtonBase';

packages/design-system-react-native/src/components/temp-components/ButtonAnimated/ButtonAnimated.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { render, fireEvent } from '@testing-library/react-native';
22
import React from 'react';
3+
import { Haptics } from 'react-native-nitro-haptics';
34

45
import { ButtonAnimated } from './ButtonAnimated';
56

67
describe('ButtonAnimated', () => {
8+
beforeEach(() => {
9+
jest.clearAllMocks();
10+
});
11+
712
it('renders correctly', () => {
813
const { getByTestId } = render(<ButtonAnimated testID="button" />);
914
expect(getByTestId('button')).not.toBeNull();
@@ -49,4 +54,26 @@ describe('ButtonAnimated', () => {
4954
fireEvent(getByTestId('button'), 'pressIn');
5055
expect(onPressInMock).not.toHaveBeenCalled();
5156
});
57+
58+
it('triggers light haptic feedback by default on press', () => {
59+
const { getByTestId } = render(<ButtonAnimated testID="button" />);
60+
fireEvent(getByTestId('button'), 'pressIn');
61+
expect(Haptics.impact).toHaveBeenCalledWith('light');
62+
});
63+
64+
it('triggers custom haptic feedback style on press', () => {
65+
const { getByTestId } = render(
66+
<ButtonAnimated testID="button" hapticFeedback="heavy" />,
67+
);
68+
fireEvent(getByTestId('button'), 'pressIn');
69+
expect(Haptics.impact).toHaveBeenCalledWith('heavy');
70+
});
71+
72+
it('does not trigger haptic feedback when set to none', () => {
73+
const { getByTestId } = render(
74+
<ButtonAnimated testID="button" hapticFeedback="none" />,
75+
);
76+
fireEvent(getByTestId('button'), 'pressIn');
77+
expect(Haptics.impact).not.toHaveBeenCalled();
78+
});
5279
});

packages/design-system-react-native/src/components/temp-components/ButtonAnimated/ButtonAnimated.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState } from 'react';
22
import type { GestureResponderEvent } from 'react-native';
33
import { Pressable } from 'react-native';
4+
import { Haptics } from 'react-native-nitro-haptics';
45
import Animated, {
56
useSharedValue,
67
useAnimatedStyle,
@@ -18,6 +19,7 @@ export const ButtonAnimated = ({
1819
disabled,
1920
style,
2021
children,
22+
hapticFeedback = 'light',
2123
...props
2224
}: ButtonAnimatedProps) => {
2325
const [isPressed, setIsPressed] = useState(false);
@@ -31,6 +33,9 @@ export const ButtonAnimated = ({
3133

3234
const onPressInHandler = (event: GestureResponderEvent) => {
3335
setIsPressed(true);
36+
if (hapticFeedback !== 'none') {
37+
Haptics.impact(hapticFeedback);
38+
}
3439
animation.value = withTiming(0.97, {
3540
duration: 100,
3641
easing: Easing.bezier(0.3, 0.8, 0.3, 1),
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
import type { PressableProps } from 'react-native';
22

3+
/**
4+
* Haptic feedback styles available for button press interactions.
5+
* Maps to `Haptics.impact()` styles from react-native-nitro-haptics.
6+
* Use `'none'` to disable haptic feedback.
7+
*/
8+
export type HapticFeedbackStyle =
9+
| 'light'
10+
| 'medium'
11+
| 'heavy'
12+
| 'soft'
13+
| 'rigid'
14+
| 'none';
15+
316
/**
417
* ButtonAnimated component props.
518
*/
6-
export type ButtonAnimatedProps = PressableProps;
19+
export type ButtonAnimatedProps = PressableProps & {
20+
/**
21+
* Optional haptic feedback style triggered on press.
22+
*
23+
* @default 'light'
24+
*/
25+
hapticFeedback?: HapticFeedbackStyle;
26+
};
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
export { ButtonAnimated } from './ButtonAnimated';
2-
export type { ButtonAnimatedProps } from './ButtonAnimated.types';
2+
export type {
3+
ButtonAnimatedProps,
4+
HapticFeedbackStyle,
5+
} from './ButtonAnimated.types';

0 commit comments

Comments
 (0)