Files
Chromacase/front/components/UI/IconButton.tsx
mathysPaul d2e1ba51c6 [ADD] LibCC ChromaCase:
- IconButton and MusicItem creation and documentation
- Update native base theme
2023-11-02 21:14:38 +01:00

260 lines
7.2 KiB
TypeScript

// Import required dependencies and components.
import { Icon } from "iconsax-react-native";
import { useRef, useState, useMemo } from "react";
import { Animated, StyleProp, TouchableOpacity, ViewStyle } from "react-native";
// Default values for the component props.
const DEFAULT_SCALE_FACTOR: number = 1.25;
const DEFAULT_ANIMATION_DURATION: number = 250;
const DEFAULT_PADDING: number = 0;
// Define the type for the IconButton props.
type IconButtonProps = {
/**
* Indicates if the button starts in an active state.
* @default false
*/
isActive?: boolean;
/**
* Color of the icon.
*/
color: string;
/**
* Optional color of the icon when active.
*/
colorActive?: string;
/**
* Callback function triggered when the button is pressed.
*/
onPress?: () => void | Promise<void>;
/**
* Size of the icon.
* @default 24
*/
size?: number;
/**
* Icon to display.
*/
icon: Icon;
/**
* Optional icon to display when active.
*/
iconActive?: Icon;
/**
* Variant style of the icon.
* @default "Outline"
*/
variant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone";
/**
* Variant style of the icon when active.
* @default "Outline"
*/
activeVariant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone";
/**
* Custom style for the icon.
*/
style?: ViewStyle | ViewStyle[];
/**
* Custom style for the icon's container.
*/
containerStyle?: ViewStyle | ViewStyle[];
/**
* Scale factor when the icon animates on press.
* @default 1.25
*/
scaleFactor?: number;
/**
* Duration of the icon animation in milliseconds.
* @default 250
*/
animationDuration?: number;
/**
* Padding around the icon.
* @default 0
*/
padding?: number;
};
/**
* `IconButton` Component
*
* Render an interactive icon that can toggle between active and inactive states.
* Supports customization of colors, icons, animation speed, size, and more.
*
* Features:
* - Can render two different icons for active and inactive states.
* - Includes an animation that scales the icon when pressed.
* - Supports custom styling for both the icon and its container.
* - Accepts various icon variants for flexibility in design.
*
* Usage:
*
* ```jsx
* <IconButton
* color="#000"
* icon={SomeIcon}
* onPress={() => console.log('Icon pressed!')}
* />
* ```
*
* To use with active states:
*
* ```jsx
* <IconButton
* isActive={true}
* color="#000"
* colorActive="#ff0000"
* icon={SomeIcon}
* iconActive={SomeActiveIcon}
* onPress={() => console.log('Icon toggled!')}
* />
* ```
*
* Note: If `iconActive` is provided but `colorActive` is not,
* the `color` prop will be used for both active and inactive states.
*/
const IconButton: React.FC<IconButtonProps> = ({
isActive = false,
color,
colorActive,
onPress,
size = 24,
icon: Icon,
iconActive: IconActive,
variant = "Outline",
activeVariant = "Outline",
style,
containerStyle,
scaleFactor = DEFAULT_SCALE_FACTOR,
animationDuration = DEFAULT_ANIMATION_DURATION,
padding = DEFAULT_PADDING,
}) => {
// State to track active status.
const [isActiveState, setIsActiveState] = useState<boolean>(isActive);
// Animation values.
const scaleValue: Animated.Value = useRef(new Animated.Value(1)).current;
const animateValue: Animated.Value = useRef(new Animated.Value(isActive ? 0 : 1)).current;
// Check for active icon.
const hasActiveIcon: boolean = !!IconActive;
// Interpolation for icon colors between active and inactive states.
const colorValue: Animated.AnimatedInterpolation<string | number> = animateValue.interpolate({
inputRange: [0, 1],
outputRange: [color, colorActive || color],
});
// Combine styles for the icon container.
const combinedContainerStyle: StyleProp<ViewStyle> = useMemo<(ViewStyle | ViewStyle[] | undefined)[]>(
() => [
{
position: 'relative',
// Adjust width and height to account for specified padding. Since the icons
// are absolutely positioned inside the container, the container's size needs
// to include the padding to ensure the icons are properly centered and spaced.
width: size + (padding * 2),
height: size + (padding * 2),
justifyContent: 'center',
alignItems: 'center',
},
containerStyle
],
[padding, containerStyle]
);
/**
* Toggles the active state of the icon.
* Executes the onPress callback and triggers the animation.
*/
const toggleState = async () => {
// Execute onPress if provided.
if (onPress) {
await onPress();
}
// Toggle isActiveState.
setIsActiveState(!isActiveState);
// Animation sequences.
const animations: Animated.CompositeAnimation[] = [
Animated.sequence([
Animated.timing(scaleValue, {
toValue: scaleFactor,
duration: animationDuration,
useNativeDriver: true,
}),
Animated.timing(scaleValue, {
toValue: 1,
duration: animationDuration,
useNativeDriver: true,
}),
]),
];
if (hasActiveIcon || colorActive) {
animations.push(
Animated.timing(animateValue, {
toValue: isActiveState ? 1 : 0,
duration: animationDuration,
useNativeDriver: true,
})
);
}
// Start animations in parallel.
Animated.parallel(animations).start();
};
return (
<TouchableOpacity
activeOpacity={1}
onPress={toggleState}
style={combinedContainerStyle}
>
{hasActiveIcon && (
<Animated.View
style={[{
padding: padding,
color: colorValue,
opacity: animateValue.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
position: 'absolute',
transform: [{ scale: scaleValue }],
}, style]}
>
<IconActive size={size} variant={activeVariant}/>
</Animated.View>
)}
<Animated.View
style={[{
padding: padding,
color: colorValue,
opacity: animateValue,
position: 'absolute',
transform: [{ scale: scaleValue }],
}, style]}
>
<Icon size={size} variant={variant} />
</Animated.View>
</TouchableOpacity>
);
};
export default IconButton;