Skip to content

Commit

Permalink
Merge pull request #746 from ephemeraHQ/saulmc/banner
Browse files Browse the repository at this point in the history
Style null states according to designs
  • Loading branch information
saulmc authored Sep 17, 2024
2 parents 7b03f25 + 2c9d2a6 commit 8d794ff
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 157 deletions.
85 changes: 85 additions & 0 deletions components/Banner/AnimatedBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Margins } from "@styles/sizes";
import React, { useState, useCallback, useRef } from "react";
import { ViewStyle, StyleProp, LayoutChangeEvent } from "react-native";
import Reanimated, {
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
Easing,
} from "react-native-reanimated";

import Banner from "./Banner";

interface AnimatedBannerProps {
title: string;
description: string;
cta?: string;
onButtonPress?: () => void;
onDismiss?: () => void;
style?: StyleProp<ViewStyle>;
}

const VERTICAL_MARGIN = Margins.default;

const AnimatedBanner: React.FC<AnimatedBannerProps> = React.memo((props) => {
const [isVisible, setIsVisible] = useState(true);
const [isAnimating, setIsAnimating] = useState(false);
const opacity = useSharedValue(1);
const height = useSharedValue(0);
const measuredHeight = useRef<number | null>(null);

const handleLayout = useCallback(
(event: LayoutChangeEvent) => {
if (isAnimating) return;

const layoutHeight = event.nativeEvent.layout.height;

if (layoutHeight > 0 && measuredHeight.current === null) {
measuredHeight.current = layoutHeight;
height.value = layoutHeight + VERTICAL_MARGIN * 2;
}
},
[height, isAnimating]
);

const handleDismiss = () => {
setIsAnimating(true);
const config = {
duration: 200,
easing: Easing.bezier(0.7, 0.0, 1, 1),
};

opacity.value = withTiming(0, config);
height.value = withTiming(0, config, (finished) => {
if (finished) {
runOnJS(setIsVisible)(false);
props.onDismiss && runOnJS(props.onDismiss)();
measuredHeight.current = null;
runOnJS(setIsAnimating)(false);
}
});
};

const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
height: isAnimating
? height.value
: measuredHeight.current !== null
? height.value
: "auto",
overflow: "hidden",
}));

if (!isVisible) {
return null; // Prevent re-rendering if not visible
}

return (
<Reanimated.View style={[animatedStyle, { zIndex: 1000 }, props.style]}>
<Banner {...props} onDismiss={handleDismiss} onLayout={handleLayout} />
</Reanimated.View>
);
});

export default AnimatedBanner;
137 changes: 137 additions & 0 deletions components/Banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import Picto from "@components/Picto/Picto";
import {
inversePrimaryColor,
primaryColor,
textSecondaryColor,
} from "@styles/colors";
import { Paddings, Margins, BorderRadius } from "@styles/sizes";
import React from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
useColorScheme,
ColorSchemeName,
StyleProp,
ViewStyle,
LayoutChangeEvent,
Platform,
} from "react-native";

interface BannerProps {
title: string;
description: string;
cta?: string;

onButtonPress?: () => void;

onDismiss: () => void;
style?: StyleProp<ViewStyle>;
onLayout?: (event: LayoutChangeEvent) => void;
}

const Banner: React.FC<BannerProps> = ({
title,
description,
cta,
onButtonPress,
onDismiss,
style,
onLayout,
}) => {
const colorScheme = useColorScheme();
const styles = useStyles(colorScheme);

return (
<View style={[styles.banner, style]} onLayout={onLayout}>
<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
</View>
{cta && (
<TouchableOpacity
onPress={onButtonPress}
style={styles.ctaButton}
accessibilityLabel={cta}
>
<Text style={styles.ctaButtonText}>{cta}</Text>
<Picto
picto="arrow.right.circle.fill"
size={Platform.OS === "ios" ? 14 : 20}
color={primaryColor(colorScheme)}
style={styles.ctaPicto}
/>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={onDismiss}
style={styles.dismissButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Picto
picto="xmark.circle.fill"
size={Platform.OS === "ios" ? 14 : 20}
color={inversePrimaryColor(colorScheme)}
/>
</TouchableOpacity>
</View>
);
};

const useStyles = (colorScheme: ColorSchemeName) => {
return StyleSheet.create({
banner: {
backgroundColor: primaryColor(colorScheme),
padding: Paddings.default,
borderRadius: BorderRadius.default,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
margin: Margins.default,
},
textContainer: {},
title: {
color: inversePrimaryColor(colorScheme),
fontSize: 16,
fontWeight: "bold",
textAlign: "center",
marginBottom: Margins.small,
letterSpacing: -0.2,
},
description: {
color: textSecondaryColor(colorScheme === "dark" ? "light" : "dark"),
fontSize: 14,
letterSpacing: -0.3,
textAlign: "center",
marginBottom: Margins.default,
},
dismissButtonContainer: {},
dismissButton: {
position: "absolute",
top: Platform.OS === "ios" ? Margins.large : Margins.default,
right: Platform.OS === "ios" ? Margins.large : Margins.default,
zIndex: 1000,
},
ctaButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
backgroundColor: inversePrimaryColor(colorScheme),
borderRadius: BorderRadius.xLarge,
paddingHorizontal: Paddings.default,
paddingVertical: Paddings.default - 6,
},
ctaButtonText: {
fontSize: 14,
fontWeight: "bold",
color: primaryColor(colorScheme),
},
ctaPicto: {
marginRight: Platform.OS === "ios" ? Margins.small + 2 : 0,
marginLeft: Platform.OS === "ios" ? Margins.default + 2 : Margins.small,
},
});
};

export default Banner;
Loading

0 comments on commit 8d794ff

Please sign in to comment.