template_0205
This commit is contained in:
349
RN_TEMPLATE/app/components/Button.tsx
Normal file
349
RN_TEMPLATE/app/components/Button.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
import { ComponentType, useEffect, useRef } from "react"
|
||||
import {
|
||||
Animated,
|
||||
Pressable,
|
||||
PressableProps,
|
||||
PressableStateCallbackType,
|
||||
StyleProp,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from "react-native"
|
||||
|
||||
import { useAppTheme } from "@/theme/context"
|
||||
import { $styles } from "@/theme/styles"
|
||||
import type { ThemedStyle, ThemedStyleArray } from "@/theme/types"
|
||||
import { s, fs } from "@/utils/responsive"
|
||||
|
||||
import { Text, TextProps } from "./Text"
|
||||
|
||||
/**
|
||||
* Three-dot loading animation component
|
||||
*/
|
||||
function LoadingDots({ color }: { color: string }) {
|
||||
const dot1 = useRef(new Animated.Value(0)).current
|
||||
const dot2 = useRef(new Animated.Value(0)).current
|
||||
const dot3 = useRef(new Animated.Value(0)).current
|
||||
|
||||
useEffect(() => {
|
||||
const animateDot = (dot: Animated.Value, delay: number) => {
|
||||
return Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.delay(delay),
|
||||
Animated.timing(dot, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(dot, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
const animation = Animated.parallel([
|
||||
animateDot(dot1, 0),
|
||||
animateDot(dot2, 150),
|
||||
animateDot(dot3, 300),
|
||||
])
|
||||
|
||||
animation.start()
|
||||
|
||||
return () => animation.stop()
|
||||
}, [dot1, dot2, dot3])
|
||||
|
||||
const dotStyle = (animatedValue: Animated.Value) => ({
|
||||
width: s(8),
|
||||
height: s(8),
|
||||
borderRadius: s(4),
|
||||
backgroundColor: color,
|
||||
marginHorizontal: 3,
|
||||
opacity: animatedValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.3, 1],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
scale: animatedValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.8, 1.2],
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "center" }}>
|
||||
<Animated.View style={dotStyle(dot1)} />
|
||||
<Animated.View style={dotStyle(dot2)} />
|
||||
<Animated.View style={dotStyle(dot3)} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
type Presets = "default" | "filled" | "reversed" | "link"
|
||||
|
||||
export interface ButtonAccessoryProps {
|
||||
style: StyleProp<any>
|
||||
pressableState: PressableStateCallbackType
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface ButtonProps extends PressableProps {
|
||||
/**
|
||||
* Text which is looked up via i18n.
|
||||
*/
|
||||
tx?: TextProps["tx"]
|
||||
/**
|
||||
* The text to display if not using `tx` or nested components.
|
||||
*/
|
||||
text?: TextProps["text"]
|
||||
/**
|
||||
* Optional options to pass to i18n. Useful for interpolation
|
||||
* as well as explicitly setting locale or translation fallbacks.
|
||||
*/
|
||||
txOptions?: TextProps["txOptions"]
|
||||
/**
|
||||
* An optional style override useful for padding & margin.
|
||||
*/
|
||||
style?: StyleProp<ViewStyle>
|
||||
/**
|
||||
* An optional style override for the "pressed" state.
|
||||
*/
|
||||
pressedStyle?: StyleProp<ViewStyle>
|
||||
/**
|
||||
* An optional style override for the button text.
|
||||
*/
|
||||
textStyle?: StyleProp<TextStyle>
|
||||
/**
|
||||
* An optional style override for the button text when in the "pressed" state.
|
||||
*/
|
||||
pressedTextStyle?: StyleProp<TextStyle>
|
||||
/**
|
||||
* An optional style override for the button text when in the "disabled" state.
|
||||
*/
|
||||
disabledTextStyle?: StyleProp<TextStyle>
|
||||
/**
|
||||
* One of the different types of button presets.
|
||||
*/
|
||||
preset?: Presets
|
||||
/**
|
||||
* An optional component to render on the right side of the text.
|
||||
* Example: `RightAccessory={(props) => <View {...props} />}`
|
||||
*/
|
||||
RightAccessory?: ComponentType<ButtonAccessoryProps>
|
||||
/**
|
||||
* An optional component to render on the left side of the text.
|
||||
* Example: `LeftAccessory={(props) => <View {...props} />}`
|
||||
*/
|
||||
LeftAccessory?: ComponentType<ButtonAccessoryProps>
|
||||
/**
|
||||
* Children components.
|
||||
*/
|
||||
children?: React.ReactNode
|
||||
/**
|
||||
* disabled prop, accessed directly for declarative styling reasons.
|
||||
* https://reactnative.dev/docs/pressable#disabled
|
||||
*/
|
||||
disabled?: boolean
|
||||
/**
|
||||
* An optional style override for the disabled state
|
||||
*/
|
||||
disabledStyle?: StyleProp<ViewStyle>
|
||||
/**
|
||||
* Show loading state with animated dots
|
||||
*/
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that allows users to take actions and make choices.
|
||||
* Wraps the Text component with a Pressable component.
|
||||
* @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Button/}
|
||||
* @param {ButtonProps} props - The props for the `Button` component.
|
||||
* @returns {JSX.Element} The rendered `Button` component.
|
||||
* @example
|
||||
* <Button
|
||||
* tx="common:ok"
|
||||
* style={styles.button}
|
||||
* textStyle={styles.buttonText}
|
||||
* onPress={handleButtonPress}
|
||||
* />
|
||||
*/
|
||||
export function Button(props: ButtonProps) {
|
||||
const {
|
||||
tx,
|
||||
text,
|
||||
txOptions,
|
||||
style: $viewStyleOverride,
|
||||
pressedStyle: $pressedViewStyleOverride,
|
||||
textStyle: $textStyleOverride,
|
||||
pressedTextStyle: $pressedTextStyleOverride,
|
||||
disabledTextStyle: $disabledTextStyleOverride,
|
||||
children,
|
||||
RightAccessory,
|
||||
LeftAccessory,
|
||||
disabled,
|
||||
disabledStyle: $disabledViewStyleOverride,
|
||||
loading,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const {
|
||||
themed,
|
||||
theme: { colors },
|
||||
} = useAppTheme()
|
||||
|
||||
const preset: Presets = props.preset ?? "default"
|
||||
|
||||
// Get loading dots color based on preset
|
||||
const loadingDotsColor =
|
||||
preset === "reversed" ? colors.palette.neutral100 : colors.palette.neutral800
|
||||
/**
|
||||
* @param {PressableStateCallbackType} root0 - The root object containing the pressed state.
|
||||
* @param {boolean} root0.pressed - The pressed state.
|
||||
* @returns {StyleProp<ViewStyle>} The view style based on the pressed state.
|
||||
*/
|
||||
function $viewStyle({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> {
|
||||
return [
|
||||
themed($viewPresets[preset]),
|
||||
$viewStyleOverride,
|
||||
!!pressed && themed([$pressedViewPresets[preset], $pressedViewStyleOverride]),
|
||||
!!disabled && $disabledViewStyleOverride,
|
||||
]
|
||||
}
|
||||
/**
|
||||
* @param {PressableStateCallbackType} root0 - The root object containing the pressed state.
|
||||
* @param {boolean} root0.pressed - The pressed state.
|
||||
* @returns {StyleProp<TextStyle>} The text style based on the pressed state.
|
||||
*/
|
||||
function $textStyle({ pressed }: PressableStateCallbackType): StyleProp<TextStyle> {
|
||||
return [
|
||||
themed($textPresets[preset]),
|
||||
$textStyleOverride,
|
||||
!!pressed && themed([$pressedTextPresets[preset], $pressedTextStyleOverride]),
|
||||
!!disabled && $disabledTextStyleOverride,
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={$viewStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: !!disabled || !!loading }}
|
||||
{...rest}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
{(state) => (
|
||||
<>
|
||||
{!!LeftAccessory && !loading && (
|
||||
<LeftAccessory style={$leftAccessoryStyle} pressableState={state} disabled={disabled} />
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<LoadingDots color={loadingDotsColor} />
|
||||
) : (
|
||||
<Text tx={tx} text={text} txOptions={txOptions} style={$textStyle(state)}>
|
||||
{children}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!!RightAccessory && !loading && (
|
||||
<RightAccessory
|
||||
style={$rightAccessoryStyle}
|
||||
pressableState={state}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const $baseViewStyle: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
minHeight: s(56),
|
||||
borderRadius: s(4),
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.sm,
|
||||
overflow: "hidden",
|
||||
})
|
||||
|
||||
const $baseTextStyle: ThemedStyle<TextStyle> = ({ typography }) => ({
|
||||
fontSize: fs(16),
|
||||
lineHeight: fs(20),
|
||||
fontFamily: typography.primary.medium,
|
||||
textAlign: "center",
|
||||
flexShrink: 1,
|
||||
flexGrow: 0,
|
||||
zIndex: 2,
|
||||
})
|
||||
|
||||
const $rightAccessoryStyle: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
marginStart: spacing.xs,
|
||||
zIndex: 1,
|
||||
})
|
||||
const $leftAccessoryStyle: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
marginEnd: spacing.xs,
|
||||
zIndex: 1,
|
||||
})
|
||||
|
||||
const $viewPresets: Record<Presets, ThemedStyleArray<ViewStyle>> = {
|
||||
default: [
|
||||
$styles.row,
|
||||
$baseViewStyle,
|
||||
({ colors }) => ({
|
||||
borderWidth: 1,
|
||||
borderColor: colors.palette.neutral400,
|
||||
backgroundColor: colors.palette.neutral100,
|
||||
}),
|
||||
],
|
||||
filled: [
|
||||
$styles.row,
|
||||
$baseViewStyle,
|
||||
({ colors }) => ({ backgroundColor: colors.palette.neutral300 }),
|
||||
],
|
||||
reversed: [
|
||||
$styles.row,
|
||||
$baseViewStyle,
|
||||
({ colors }) => ({ backgroundColor: colors.palette.neutral800 }),
|
||||
],
|
||||
link: [
|
||||
$styles.row,
|
||||
() => ({
|
||||
minHeight: 0,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
const $textPresets: Record<Presets, ThemedStyleArray<TextStyle>> = {
|
||||
default: [$baseTextStyle],
|
||||
filled: [$baseTextStyle],
|
||||
reversed: [$baseTextStyle, ({ colors }) => ({ color: colors.palette.neutral100 })],
|
||||
link: [({ colors }) => ({ color: colors.tint, fontSize: fs(14) })],
|
||||
}
|
||||
|
||||
const $pressedViewPresets: Record<Presets, ThemedStyle<ViewStyle>> = {
|
||||
default: ({ colors }) => ({ backgroundColor: colors.palette.neutral200 }),
|
||||
filled: ({ colors }) => ({ backgroundColor: colors.palette.neutral400 }),
|
||||
reversed: ({ colors }) => ({ backgroundColor: colors.palette.neutral700 }),
|
||||
link: () => ({ opacity: 0.7 }),
|
||||
}
|
||||
|
||||
const $pressedTextPresets: Record<Presets, ThemedStyle<TextStyle>> = {
|
||||
default: () => ({ opacity: 0.9 }),
|
||||
filled: () => ({ opacity: 0.9 }),
|
||||
reversed: () => ({ opacity: 0.9 }),
|
||||
link: () => ({}),
|
||||
}
|
||||
Reference in New Issue
Block a user