Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[C-3427] Add BaseButton to mobile harmony #6968

Merged
merged 1 commit into from
Dec 20, 2023
Merged
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
Original file line number Diff line number Diff line change
@@ -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)
)<BaseButtonProps>(({ 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<number | null>(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 (
<GestureDetector gesture={taps}>
<Root
style={[rootStyles, style]}
onLayout={handleLayoutChange}
{...other}
>
{isLoading ? (
<LoadingSpinner style={[{ height: 16, width: 16 }, styles?.icon]} />
) : LeftIconComponent ? (
<LeftIconComponent style={styles?.icon} size='s' color='default' />
) : null}
{!isTextHidden ? <Text>{children}</Text> : null}
{RightIconComponent ? (
<RightIconComponent style={styles?.icon} size='s' color='default' />
) : null}
</Root>
</GestureDetector>
)
}
64 changes: 64 additions & 0 deletions packages/mobile/src/harmony-native/components/Button/types.ts
Original file line number Diff line number Diff line change
@@ -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<ViewStyle>
icon?: StyleProp<ViewStyle>
}

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<PressableProps, 'children' | 'style'>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Flex } from '../layout/Flex/Flex'
import { CompletionCheck } from './CompletionCheck'

const meta: Meta<CompletionCheckProps> = {
title: 'Components/Input/CompletionCheck',
title: 'Components/CompletionCheck',
component: CompletionCheck,
argTypes: {
value: {
Expand Down