diff --git a/packages/mobile/src/harmony-native/components/Button/BaseButton/BaseButton.tsx b/packages/mobile/src/harmony-native/components/Button/BaseButton/BaseButton.tsx new file mode 100644 index 00000000000..a79fce26088 --- /dev/null +++ b/packages/mobile/src/harmony-native/components/Button/BaseButton/BaseButton.tsx @@ -0,0 +1,112 @@ +import { useCallback, useState } from 'react' + +import styled from '@emotion/native' +import type { LayoutChangeEvent } from 'react-native' +import { Pressable } from 'react-native' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import Animated, { + Easing, + interpolate, + useAnimatedStyle, + useSharedValue, + withTiming +} from 'react-native-reanimated' + +import LoadingSpinner from 'app/components/loading-spinner' + +import { Text } from '../../Text/Text' +import type { BaseButtonProps } from '../types' + +const animationConfig = { + duration: 120, + easing: Easing.bezier(0.44, 0, 0.56, 1) +} + +const Root = styled( + Animated.createAnimatedComponent(Pressable) +)(({ theme, minWidth, disabled, fullWidth }) => ({ + position: 'relative', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + boxSizing: 'border-box', + flexShrink: 0, + justifyContent: 'center', + overflow: 'hidden', + minWidth: minWidth && `${minWidth}px`, + + // TODO: This might not be needed, but here now for testing + opacity: disabled ? 0.45 : 1, + + ...(fullWidth && { + width: '100%', + flexShrink: 1 + }) +})) + +export const BaseButton = (props: BaseButtonProps) => { + const { + iconLeft: LeftIconComponent, + iconRight: RightIconComponent, + isLoading, + widthToHideText, + styles, + children, + style, + ...other + } = props + const pressed = useSharedValue(0) + const [buttonWidth, setButtonWidth] = useState(null) + + const isTextHidden = + widthToHideText && buttonWidth && buttonWidth <= widthToHideText + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = withTiming(1, animationConfig) + }) + .onFinalize(() => { + pressed.value = withTiming(0, animationConfig) + }) + + const longPress = Gesture.LongPress() + .minDuration(animationConfig.duration) + .onBegin(() => { + pressed.value = withTiming(1, animationConfig) + }) + .onFinalize(() => { + pressed.value = withTiming(0, animationConfig) + }) + + const taps = Gesture.Exclusive(longPress, tap) + + const rootStyles = useAnimatedStyle(() => ({ + transform: [{ scale: interpolate(pressed.value, [0, 1], [1, 0.97]) }] + })) + + const handleLayoutChange = useCallback((event: LayoutChangeEvent) => { + const { width } = event.nativeEvent.layout + setButtonWidth(width) + }, []) + + return ( + + + {isLoading ? ( + + ) : LeftIconComponent ? ( + + ) : null} + {!isTextHidden ? {children} : null} + {RightIconComponent ? ( + + ) : null} + + + ) +} diff --git a/packages/mobile/src/harmony-native/components/Button/types.ts b/packages/mobile/src/harmony-native/components/Button/types.ts new file mode 100644 index 00000000000..6a04420d40f --- /dev/null +++ b/packages/mobile/src/harmony-native/components/Button/types.ts @@ -0,0 +1,64 @@ +import type { ReactNode } from 'react' + +import type { PressableProps, StyleProp, ViewStyle } from 'react-native/types' + +import type { Icon } from 'app/harmony-native/icons' + +type BaseButtonStyles = { + button?: StyleProp + icon?: StyleProp +} + +export type BaseButtonProps = { + /** + * Optional icon element to include on the left side of the button + */ + iconLeft?: Icon + + /** + * Optional icon element to include on the right side of the button + */ + iconRight?: Icon + + /** + * When true, do not override icon's fill colors + */ + isStaticIcon?: boolean + + /** + * Show a spinning loading state instead of the left icon + */ + isLoading?: boolean + + /** + * The max width at which text will still be shown + */ + widthToHideText?: number + + /** + * Optional min width + * Min width can be useful if the button is switching states and you want + * to keep a certain width while text length changes + */ + minWidth?: number + + /** + * If provided, allow button to take up full width of container + */ + fullWidth?: boolean + + /** + * Internal styling used by derived button components + */ + styles?: BaseButtonStyles + + /** + * Native styling for the pressable component + */ + style?: ViewStyle + + /** + * Child elements + */ + children?: ReactNode +} & Omit diff --git a/packages/mobile/src/harmony-native/components/CompletionCheck/CompletionCheck.stories.tsx b/packages/mobile/src/harmony-native/components/CompletionCheck/CompletionCheck.stories.tsx index 1576437ddbb..75c15560d3d 100644 --- a/packages/mobile/src/harmony-native/components/CompletionCheck/CompletionCheck.stories.tsx +++ b/packages/mobile/src/harmony-native/components/CompletionCheck/CompletionCheck.stories.tsx @@ -6,7 +6,7 @@ import { Flex } from '../layout/Flex/Flex' import { CompletionCheck } from './CompletionCheck' const meta: Meta = { - title: 'Components/Input/CompletionCheck', + title: 'Components/CompletionCheck', component: CompletionCheck, argTypes: { value: {