diff --git a/src/__tests__/PressableSurface.test.tsx b/src/__tests__/PressableSurface.test.tsx new file mode 100644 index 0000000..165c48b --- /dev/null +++ b/src/__tests__/PressableSurface.test.tsx @@ -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"'); + }); +}); diff --git a/src/components/PressableSurface.tsx b/src/components/PressableSurface.tsx new file mode 100644 index 0000000..6b10983 --- /dev/null +++ b/src/components/PressableSurface.tsx @@ -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; + 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 ( + + + {children} + + + ); +} + +const styles = StyleSheet.create({ + base: { + alignSelf: 'stretch', + }, +});