Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/design-system-react-native/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = merge(baseConfig, {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!react-native|@react-native|react-native-reanimated|@react-navigation)',
'node_modules/(?!react-native|@react-native|react-native-reanimated|react-native-nitro-haptics|react-native-nitro-modules|@react-navigation)',
],
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
moduleNameMapper: {
Expand Down
8 changes: 8 additions & 0 deletions packages/design-system-react-native/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ jest.mock('react-native-svg', () => {
};
});

jest.mock('react-native-nitro-haptics', () => ({
Haptics: {
impact: jest.fn(),
notification: jest.fn(),
selection: jest.fn(),
},
}), { virtual: true });

jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');

Expand Down
4 changes: 4 additions & 0 deletions packages/design-system-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"react": "^18.2.0",
"react-native": "^0.72.15",
"react-native-gesture-handler": "2.12.0",
"react-native-nitro-haptics": "^0.2.3",
"react-native-nitro-modules": "^0.25.0",
"react-native-reanimated": "~3.3.0",
"react-native-safe-area-context": "4.14.1",
"react-native-svg": "^15.10.1",
Expand All @@ -96,6 +98,8 @@
"react": ">=18.2.0",
"react-native": ">=0.72.0",
"react-native-gesture-handler": ">=1.10.3",
"react-native-nitro-haptics": ">=0.3.0",
"react-native-nitro-modules": ">=0.25.0",
"react-native-reanimated": ">=3.3.0",
"react-native-safe-area-context": ">=4.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const ButtonBase = ({
accessibilityRole = 'button',
accessibilityActions,
onAccessibilityAction,
hapticFeedback,
...props
}: ButtonBaseProps) => {
const tw = useTailwind();
Expand Down Expand Up @@ -93,6 +94,7 @@ export const ButtonBase = ({
return (
<ButtonAnimated
disabled={isDisabled || isLoading}
hapticFeedback={hapticFeedback}
accessibilityRole={accessibilityRole}
accessibilityLabel={finalAccessibilityLabel}
accessibilityHint={finalAccessibilityHint}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PressableProps, StyleProp, ViewStyle } from 'react-native';

import type { ButtonBaseSize } from '../../types';
import type { IconProps, IconName } from '../Icon';
import type { HapticFeedbackStyle } from '../temp-components/ButtonAnimated';
import type { SpinnerProps } from '../temp-components/Spinner';
import type { TextProps } from '../Text';

Expand Down Expand Up @@ -120,6 +121,12 @@ export type ButtonBaseProps = {
onAccessibilityAction?: (event: {
nativeEvent: { actionName: string };
}) => void;
/**
* Optional haptic feedback style triggered on press.
*
* @default 'light'
*/
hapticFeedback?: HapticFeedbackStyle;
} & Omit<
PressableProps,
| 'accessibilityRole'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ export { Card } from './Card';
export type { CardProps } from './Card';

export { ButtonAnimated } from './temp-components/ButtonAnimated';
export type { ButtonAnimatedProps } from './temp-components/ButtonAnimated';
export type {
ButtonAnimatedProps,
HapticFeedbackStyle,
} from './temp-components/ButtonAnimated';

export { ButtonBase, ButtonBaseSize } from './ButtonBase';
export type { ButtonBaseProps } from './ButtonBase';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { render, fireEvent } from '@testing-library/react-native';
import React from 'react';
import { Haptics } from 'react-native-nitro-haptics';

import { ButtonAnimated } from './ButtonAnimated';

describe('ButtonAnimated', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders correctly', () => {
const { getByTestId } = render(<ButtonAnimated testID="button" />);
expect(getByTestId('button')).not.toBeNull();
Expand Down Expand Up @@ -49,4 +54,26 @@ describe('ButtonAnimated', () => {
fireEvent(getByTestId('button'), 'pressIn');
expect(onPressInMock).not.toHaveBeenCalled();
});

it('triggers light haptic feedback by default on press', () => {
const { getByTestId } = render(<ButtonAnimated testID="button" />);
fireEvent(getByTestId('button'), 'pressIn');
expect(Haptics.impact).toHaveBeenCalledWith('light');
});

it('triggers custom haptic feedback style on press', () => {
const { getByTestId } = render(
<ButtonAnimated testID="button" hapticFeedback="heavy" />,
);
fireEvent(getByTestId('button'), 'pressIn');
expect(Haptics.impact).toHaveBeenCalledWith('heavy');
});

it('does not trigger haptic feedback when set to none', () => {
const { getByTestId } = render(
<ButtonAnimated testID="button" hapticFeedback="none" />,
);
fireEvent(getByTestId('button'), 'pressIn');
expect(Haptics.impact).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import type { GestureResponderEvent } from 'react-native';
import { Pressable } from 'react-native';
import { Haptics } from 'react-native-nitro-haptics';
import Animated, {
useSharedValue,
useAnimatedStyle,
Expand All @@ -18,6 +19,7 @@ export const ButtonAnimated = ({
disabled,
style,
children,
hapticFeedback = 'light',
...props
}: ButtonAnimatedProps) => {
const [isPressed, setIsPressed] = useState(false);
Expand All @@ -31,6 +33,9 @@ export const ButtonAnimated = ({

const onPressInHandler = (event: GestureResponderEvent) => {
setIsPressed(true);
if (hapticFeedback !== 'none') {
Haptics.impact(hapticFeedback);
}
animation.value = withTiming(0.97, {
duration: 100,
easing: Easing.bezier(0.3, 0.8, 0.3, 1),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import type { PressableProps } from 'react-native';

/**
* Haptic feedback styles available for button press interactions.
* Maps to `Haptics.impact()` styles from react-native-nitro-haptics.
* Use `'none'` to disable haptic feedback.
*/
export type HapticFeedbackStyle =
| 'light'
| 'medium'
| 'heavy'
| 'soft'
| 'rigid'
| 'none';

/**
* ButtonAnimated component props.
*/
export type ButtonAnimatedProps = PressableProps;
export type ButtonAnimatedProps = PressableProps & {
/**
* Optional haptic feedback style triggered on press.
*
* @default 'light'
*/
hapticFeedback?: HapticFeedbackStyle;
};
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export { ButtonAnimated } from './ButtonAnimated';
export type { ButtonAnimatedProps } from './ButtonAnimated.types';
export type {
ButtonAnimatedProps,
HapticFeedbackStyle,
} from './ButtonAnimated.types';
25 changes: 25 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3401,6 +3401,8 @@ __metadata:
react-native: "npm:^0.72.15"
react-native-gesture-handler: "npm:2.12.0"
react-native-jazzicon: "npm:^0.1.2"
react-native-nitro-haptics: "npm:^0.2.3"
react-native-nitro-modules: "npm:^0.25.0"
react-native-reanimated: "npm:~3.3.0"
react-native-safe-area-context: "npm:4.14.1"
react-native-svg: "npm:^15.10.1"
Expand All @@ -3417,6 +3419,8 @@ __metadata:
react: ">=18.2.0"
react-native: ">=0.72.0"
react-native-gesture-handler: ">=1.10.3"
react-native-nitro-haptics: ">=0.3.0"
react-native-nitro-modules: ">=0.25.0"
react-native-reanimated: ">=3.3.0"
react-native-safe-area-context: ">=4.0.0"
languageName: unknown
Expand Down Expand Up @@ -18194,6 +18198,27 @@ __metadata:
languageName: node
linkType: hard

"react-native-nitro-haptics@npm:^0.2.3":
version: 0.2.3
resolution: "react-native-nitro-haptics@npm:0.2.3"
peerDependencies:
react: "*"
react-native: "*"
react-native-nitro-modules: "*"
checksum: 10/7f17b002cd2d28a7b6669a454cb4943b18331fd3b2fd4b10ea3daf6cd051a4fb90c69e80d637968d6b96e7c1ec2adb3e28ccf114b9f0d143c30d5a514eb53d6b
languageName: node
linkType: hard

"react-native-nitro-modules@npm:^0.25.0":
version: 0.25.2
resolution: "react-native-nitro-modules@npm:0.25.2"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/45f0c639d21da55302e641cc6a36ffd388efb3233d12d804807f422a37387079bda713e59482ab5a5cf7c0b867a0d68bc6df729c15a2e6035c2b2cbb87a0e28a
languageName: node
linkType: hard

"react-native-reanimated@npm:~3.3.0":
version: 3.3.0
resolution: "react-native-reanimated@npm:3.3.0"
Expand Down
Loading