import { MeasuredView, ViewMeasurements } from "../../utils/MeasuredView"
import { Spacer } from "../Spacer"
import { useColor } from "../../hooks"
import { useState } from "react"
import { PressableProps, TextStyle } from "react-native"
import { GestureResponderEvent, Pressable } from "react-native"
import Haptic, { HapticFeedbackTypes } from "react-native-haptic-feedback"
import { config } from "react-spring"
// @ts-ignore
import { animated, Spring } from "react-spring/renderprops-native"
import styled from "styled-components/native"
import { Box, BoxProps } from "../Box"
import { Flex } from "../Flex"
import { Spinner } from "../Spinner"
import { Text, useTextStyleForPalette } from "../Text"
export interface ButtonProps extends BoxProps {
children: React.ReactNode
size?: "small" | "large"
variant?:
| "fillDark"
| "fillLight"
| "fillGray"
| "fillSuccess"
| "outline"
| "outlineGray"
| "outlineLight"
| "text"
onPress?: PressableProps["onPress"]
icon?: React.ReactNode
iconPosition?: "left" | "left-start" | "right"
/**
* `haptic` can be used like:
*
* or
*
* to add haptic feedback on the button.
*/
haptic?: HapticFeedbackTypes | true
/** Displays a loader in the button */
loading?: boolean
/** Disabled interactions */
disabled?: boolean
/** Makes button full width */
block?: boolean
/** Pass the longest text to the button for the button to keep longest text width */
longestText?: string
/** Used only for tests and stories */
testOnly_state?: DisplayState
}
enum DisplayState {
Enabled = "enabled",
Disabled = "disabled",
Loading = "loading",
Pressed = "pressed",
}
export const Button: React.FC = ({
children,
disabled,
haptic,
icon,
iconPosition = "left",
loading,
longestText,
onPress,
size = "large",
style,
variant = "fillDark",
testOnly_state,
testID,
...rest
}) => {
const textStyle = useTextStyleForPalette(size === "small" ? "xs" : "sm")
const [innerDisplayState, setInnerDisplayState] = useState(DisplayState.Enabled)
const [longestTextMeasurements, setLongestTextMeasurements] = useState({
width: 0,
height: 0,
})
const displayState =
testOnly_state ?? // if we use the test prop, use that
(loading // if we have loading or disabled in props, they are used
? DisplayState.Loading
: disabled
? DisplayState.Disabled
: innerDisplayState) // otherwise use the inner state for pressed or enabled
const getSize = (): { height: number; mx: number } => {
switch (size) {
case "small":
return { height: 30, mx: 15 }
case "large":
return { height: 50, mx: 30 }
}
}
const spinnerColor = variant === "text" ? "blue100" : "white100"
const handlePress = (event: GestureResponderEvent) => {
if (onPress === undefined || onPress === null) {
return
}
if (displayState === DisplayState.Loading || displayState === DisplayState.Disabled) {
return
}
// Did someone tap really fast? Flick the highlighted state
if (displayState === DisplayState.Enabled) {
setInnerDisplayState(DisplayState.Pressed)
setTimeout(() => {
setInnerDisplayState(DisplayState.Enabled)
}, 0.3)
} else {
// Was already selected
setInnerDisplayState(DisplayState.Enabled)
}
if (haptic !== undefined) {
Haptic.trigger(haptic === true ? "impactLight" : haptic)
}
onPress(event)
}
const containerSize = getSize()
const to = useStyleForVariantAndState(variant, testOnly_state ?? displayState)
return (
{(springProps: typeof to) => (
{
if (displayState === DisplayState.Loading) {
return
}
setInnerDisplayState(DisplayState.Pressed)
}}
onPressOut={() => {
if (displayState === DisplayState.Loading) {
return
}
setInnerDisplayState(DisplayState.Enabled)
}}
onPress={handlePress}
testID={testID}
>
{iconPosition === "left-start" && !!icon ? (
{icon}
) : null}
{iconPosition === "left" && !!icon ? (
<>
{icon}
>
) : null}
{longestText ? longestText : children}
{children}
{iconPosition === "right" && !!icon ? (
<>
{icon}
>
) : null}
{displayState === DisplayState.Loading ? (
) : null}
)}
)
}
const useStyleForVariantAndState = (
variant: Exclude,
state: DisplayState
): {
backgroundColor: string
borderColor: string
borderWidth?: number
textColor: string
textDecorationLine?: TextStyle["textDecorationLine"]
} => {
const color = useColor()
const retval = {
textDecorationLine: "none",
} as ReturnType
if (state === DisplayState.Loading) {
retval.backgroundColor = variant === "text" ? color("black10") : color("blue100")
retval.borderColor = "rgba(0, 0, 0, 0)"
retval.borderWidth = 0
retval.textColor = "rgba(0, 0, 0, 0)"
return retval
}
switch (variant) {
case "fillDark":
retval.textColor = color("white100")
switch (state) {
case DisplayState.Enabled:
retval.backgroundColor = color("black100")
retval.borderColor = color("black100")
break
case DisplayState.Disabled:
retval.backgroundColor = color("black30")
retval.borderColor = color("black30")
break
case DisplayState.Pressed:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
retval.textDecorationLine = "underline"
break
}
break
case "fillLight":
switch (state) {
case DisplayState.Enabled:
retval.backgroundColor = color("white100")
retval.borderColor = color("white100")
retval.textColor = color("black100")
break
case DisplayState.Disabled:
retval.backgroundColor = color("black30")
retval.borderColor = color("black30")
retval.textColor = color("white100")
break
case DisplayState.Pressed:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
retval.textColor = color("white100")
retval.textDecorationLine = "underline"
break
}
break
case "fillGray":
switch (state) {
case DisplayState.Enabled:
retval.backgroundColor = color("black10")
retval.borderColor = color("black10")
retval.textColor = color("black100")
break
case DisplayState.Disabled:
retval.backgroundColor = color("black30")
retval.borderColor = color("black30")
retval.textColor = color("white100")
break
case DisplayState.Pressed:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
retval.textColor = color("white100")
retval.textDecorationLine = "underline"
break
}
break
case "fillSuccess":
retval.textColor = color("white100")
switch (state) {
case DisplayState.Enabled:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
break
case DisplayState.Disabled:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
break
case DisplayState.Pressed:
retval.backgroundColor = color("blue10")
retval.borderColor = color("blue10")
retval.textDecorationLine = "underline"
break
default:
assertNever(state)
}
break
case "outline":
switch (state) {
case DisplayState.Enabled:
retval.backgroundColor = color("white100")
retval.borderColor = color("black60")
retval.textColor = color("black100")
break
case DisplayState.Disabled:
retval.backgroundColor = color("white100")
retval.borderColor = color("black30")
retval.textColor = color("black30")
break
case DisplayState.Pressed:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
retval.textColor = color("white100")
retval.textDecorationLine = "underline"
break
default:
assertNever(state)
}
break
case "outlineGray":
switch (state) {
case DisplayState.Enabled:
retval.backgroundColor = color("white100")
retval.borderColor = color("black30")
retval.textColor = color("black100")
break
case DisplayState.Disabled:
retval.backgroundColor = color("white100")
retval.borderColor = color("black30")
retval.textColor = color("black30")
break
case DisplayState.Pressed:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
retval.textColor = color("white100")
retval.textDecorationLine = "underline"
break
}
break
case "outlineLight":
switch (state) {
case DisplayState.Enabled:
retval.backgroundColor = "rgba(0, 0, 0, 0)"
retval.borderColor = color("white100")
retval.textColor = color("white100")
break
case DisplayState.Disabled:
retval.backgroundColor = "rgba(0, 0, 0, 0)"
retval.borderColor = color("black30")
retval.textColor = color("black30")
break
case DisplayState.Pressed:
retval.backgroundColor = color("blue100")
retval.borderColor = color("blue100")
retval.textColor = color("white100")
retval.textDecorationLine = "underline"
break
}
break
case "text":
retval.backgroundColor = "rgba(0, 0, 0, 0)"
retval.borderColor = "rgba(0, 0, 0, 0)"
switch (state) {
case DisplayState.Enabled:
retval.textColor = color("black100")
break
case DisplayState.Disabled:
retval.textColor = color("black30")
break
case DisplayState.Pressed:
retval.backgroundColor = color("black10")
retval.borderColor = color("black10")
retval.textColor = color("blue100")
retval.textDecorationLine = "underline"
break
}
break
}
return retval
}
const Container = styled(Box)`
position: relative;
border-width: 1;
border-radius: 50;
width: ${(p) => (p.block ? "100%" : "auto")};
overflow: hidden;
`
const SpinnerContainer = styled(Box)`
position: absolute;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
`
const AnimatedContainer = animated(Container)
const AnimatedText = animated(Text)
export { DisplayState as _test_DisplayState }