feat(components): PressableSurface with refined press feedback

This commit is contained in:
fightrate-bot committed 2026-06-16 01:47:37 -03:00
1 parent 9b02c10926
commit fc42e1da43
2 files changed
+113

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
import React from 'react';
import TestRenderer from 'react-test-renderer';
jest.mock('react-native-reanimated', () => {
const { View } = require('react-native');
return {
__esModule: true,
default: { View: View, createAnimatedComponent: (c: unknown) => c },
Easing: { out: () => (() => 0), inOut: () => (() => 0), quad: () => 0 },
useSharedValue: (initial: number) => ({ value: initial }),
useAnimatedStyle: (fn: () => unknown) => fn(),
withTiming: (to: number) => to,
withRepeat: (to: number) => to,
cancelAnimation: () => undefined,
};
});
import { PressableSurface } from '../components/PressableSurface';
describe('PressableSurface', () => {
it('renders children inside', () => {
let tree: any;
TestRenderer.act(() => {
tree = TestRenderer.create(
React.createElement(PressableSurface, { onPress: () => undefined, testID: 'surface' },
React.createElement('Text', null, 'Hi'),
),
);
});
const json = tree.toJSON();
expect(JSON.stringify(json)).toContain('Hi');
});
it('has accessibilityRole=button when accessibilityLabel is set', () => {
let tree: any;
TestRenderer.act(() => {
tree = TestRenderer.create(
React.createElement(PressableSurface, { onPress: () => undefined, accessibilityLabel: 'Tap', testID: 'surface' },
React.createElement('Text', null, 'Hi'),
),
);
});
const json = JSON.stringify(tree.toJSON());
expect(json).toContain('"accessibilityLabel":"Tap"');
});
});
+67
View File
@@ -0,0 +1,67 @@
import React, { useCallback, type ReactNode } from 'react';
import { Pressable, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
type Props = {
readonly children: ReactNode;
readonly onPress?: () => void;
readonly disabled?: boolean;
readonly style?: StyleProp<ViewStyle>;
readonly accessibilityLabel?: string;
readonly testID?: string;
};
export function PressableSurface({
children,
onPress,
disabled,
style,
accessibilityLabel,
testID,
}: Props) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const onPressIn = useCallback(() => {
scale.value = withTiming(0.96, { duration: 80, easing: Easing.out(Easing.quad) });
opacity.value = withTiming(0.82, { duration: 80, easing: Easing.out(Easing.quad) });
}, [scale, opacity]);
const onPressOut = useCallback(() => {
scale.value = withTiming(1, { duration: 160, easing: Easing.out(Easing.quad) });
opacity.value = withTiming(1, { duration: 160, easing: Easing.out(Easing.quad) });
}, [scale, opacity]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
return (
<Animated.View style={animatedStyle}>
<Pressable
accessibilityLabel={accessibilityLabel}
accessibilityRole={accessibilityLabel ? 'button' : undefined}
disabled={disabled}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={[styles.base, style]}
testID={testID}
>
{children}
</Pressable>
</Animated.View>
);
}
const styles = StyleSheet.create({
base: {
alignSelf: 'stretch',
},
});