diff --git a/src/components/Advice/Advice.tsx b/src/components/Advice/Advice.tsx new file mode 100644 index 00000000..37824261 --- /dev/null +++ b/src/components/Advice/Advice.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { View, StyleSheet } from "react-native"; +import { IOIconSizeScale, IOIcons, Icon } from "../icons"; +import { IOColors, IOStyles } from "../../core"; +import { HSpacer } from "../spacer"; +import { Body } from "../typography"; + +interface Props { + text: string; + iconName?: IOIcons; + iconSize?: IOIconSizeScale; + iconColor?: IOColors; +}; + +const styles = StyleSheet.create({ + icon: { + marginTop: 4 + } +}); + +const defaultIconSize: IOIconSizeScale = 20; + +/** + * This component displays an info icon on top-left and a text message + * @constructor + */ +const AdviceComponent: React.FC = ({ + text, + iconName = "notice", + iconSize = defaultIconSize, + iconColor = "blue" +}) => ( + + + + + + {text} + +); + +export default React.memo(AdviceComponent); diff --git a/src/components/Advice/__test__/__snapshots__/advice.test.tsx.snap b/src/components/Advice/__test__/__snapshots__/advice.test.tsx.snap new file mode 100644 index 00000000..5668a004 --- /dev/null +++ b/src/components/Advice/__test__/__snapshots__/advice.test.tsx.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test Advice Components Advice Snapshot 1`] = ` + + + + + + + + + + + Text + + +`; diff --git a/src/components/Advice/__test__/advice.test.tsx b/src/components/Advice/__test__/advice.test.tsx new file mode 100644 index 00000000..efffcedf --- /dev/null +++ b/src/components/Advice/__test__/advice.test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import * as TestRenderer from "react-test-renderer"; +import Advice from "../Advice"; + +describe("Test Advice Components", () => { + it("Advice Snapshot", () => { + const advice = TestRenderer.create().toJSON(); + expect(advice).toMatchSnapshot(); + }); +}); diff --git a/src/components/Advice/index.tsx b/src/components/Advice/index.tsx new file mode 100644 index 00000000..a954a738 --- /dev/null +++ b/src/components/Advice/index.tsx @@ -0,0 +1 @@ +export * from "./Advice"; \ No newline at end of file diff --git a/src/components/banner/Banner.tsx b/src/components/banner/Banner.tsx new file mode 100644 index 00000000..56d31559 --- /dev/null +++ b/src/components/banner/Banner.tsx @@ -0,0 +1,277 @@ +import React, { useCallback } from "react"; +import { + AccessibilityRole, + GestureResponderEvent, + Pressable, + StyleSheet, + View, + ViewStyle +} from "react-native"; +import Animated, { + Extrapolate, + interpolate, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withSpring +} from "react-native-reanimated"; +import { IOBannerBigSpacing, IOBannerRadius, IOBannerSmallHSpacing, IOBannerSmallVSpacing, IOScaleValues, IOSpringValues, IOStyles } from "../../core"; +import { IOColors } from "../../core/IOColors"; +import { WithTestID } from "../../utils/types"; +import { ButtonLink, IconButton } from "../buttons"; +import { IOPictogramSizeScale, IOPictogramsBleed, Pictogram } from "../pictograms"; +import { VSpacer } from "../spacer"; +import { H6, LabelSmall } from "../typography"; + + +/* Styles */ + +const colorTitle: IOColors = "blueIO-850"; +const colorContent: IOColors = "grey-700"; +const colorCloseButton: IconButton["color"] = "neutral"; +const sizePictogramBig: IOPictogramSizeScale = 80; +const sizePictogramSmall: IOPictogramSizeScale = 64; +const closeButtonDistanceFromEdge: number = 4; +const closeButtonOpacity = 0.6; +const sizeBigPadding = IOBannerBigSpacing; +const sizeSmallHPadding = IOBannerSmallHSpacing; +const sizeSmallVPadding = IOBannerSmallVSpacing; + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + alignItems: "flex-start", + alignContent: "center", + borderRadius: IOBannerRadius + }, + bleedPictogram: { + marginRight: -sizeBigPadding + }, + closeIconButton: { + position: "absolute", + right: closeButtonDistanceFromEdge, + top: closeButtonDistanceFromEdge, + opacity: closeButtonOpacity + } +}); + +const dynamicContainerStyles = ( + size: BaseBannerProps["size"], + color: BaseBannerProps["color"] +): ViewStyle => ({ + backgroundColor: IOColors[mapBackgroundColor[color]], + paddingVertical: size === "big" ? sizeBigPadding : sizeSmallVPadding, + paddingHorizontal: size === "big" ? sizeBigPadding : sizeSmallHPadding +}); + +/* Component Types */ + +type BaseBannerProps = WithTestID<{ + size: "big" | "small"; + color: "neutral" | "turquoise"; + pictogramName: IOPictogramsBleed; + viewRef: React.RefObject; + // A11y related props + accessibilityLabel?: string; + accessibilityHint?: string; +}>; + +/* Description only */ +type BannerPropsDescOnly = { title: never; content?: string }; +/* Title only */ +type BannerPropsTitleOnly = { title?: string; content: never }; +/* Title + Description */ +type BannerPropsTitleAndDesc = { title?: string; content?: string }; + +type RequiredBannerProps = + | BannerPropsDescOnly + | BannerPropsTitleOnly + | BannerPropsTitleAndDesc; + +type BannerActionProps = + | { + action?: string; + onPress: (event: GestureResponderEvent) => void; + accessibilityRole?: never; + } + | { + action?: never; + onPress?: never; + accessibilityRole?: AccessibilityRole; + }; + +// Banner will display a close button if this event is provided +type BannerCloseProps = + | { + onClose?: (event: GestureResponderEvent) => void; + labelClose?: string; + } + | { + onClose?: never; + labelClose?: never; + }; + +export type Banner = BaseBannerProps & + RequiredBannerProps & + BannerActionProps & + BannerCloseProps; + +// COMPONENT CONFIGURATION + +/* Used to generate automatically the colour variants +in the Design System screen */ +export const bannerBackgroundColours: Array = [ + "neutral", + "turquoise" +]; + +const mapBackgroundColor: Record< + NonNullable, + IOColors +> = { + neutral: "grey-50", + turquoise: "turquoise-50" +}; + +export const Banner = ({ + viewRef, + size, + color, + pictogramName, + title, + content, + action, + labelClose, + onPress, + onClose, + accessibilityHint, + accessibilityLabel, + testID +}: Banner) => { + const isPressed: Animated.SharedValue = useSharedValue(0); + + // Scaling transformation applied when the button is pressed + const animationScaleValue = IOScaleValues?.magnifiedButton?.pressedState; + + // Using a spring-based animation for our interpolations + const progressPressed = useDerivedValue(() => + withSpring(isPressed.value, IOSpringValues.button) + ); + + // Interpolate animation values from `isPressed` values + const pressedAnimationStyle = useAnimatedStyle(() => { + // Link color states to the pressed states + + // Scale down button slightly when pressed + const scale = interpolate( + progressPressed.value, + [0, 1], + [1, animationScaleValue], + Extrapolate.CLAMP + ); + + return { + transform: [{ scale }] + }; + }); + + const onPressIn = useCallback(() => { + // eslint-disable-next-line functional/immutable-data + isPressed.value = 1; + }, [isPressed]); + const onPressOut = useCallback(() => { + // eslint-disable-next-line functional/immutable-data + isPressed.value = 0; + }, [isPressed]); + + const renderMainBlock = () => ( + <> + + {title && ( + <> + {/* Once we get 'gap' property, we can get rid of + these components */} +
+ {title} +
+ + + )} + {content && ( + <> + + {content} + + {action && } + + )} + {action && ( + /* Disable pointer events to avoid + pressed state on the button */ + + + + + )} +
+ + + + {onClose && labelClose && ( + + + + )} + + ); + + const PressableButton = () => ( + + + {renderMainBlock()} + + + ); + + const StaticComponent = () => ( + + {renderMainBlock()} + + ); + + return action ? : ; +}; diff --git a/src/components/banner/__test__/__snapshots__/banner.test.tsx.snap b/src/components/banner/__test__/__snapshots__/banner.test.tsx.snap new file mode 100644 index 00000000..f54dbbbe --- /dev/null +++ b/src/components/banner/__test__/__snapshots__/banner.test.tsx.snap @@ -0,0 +1,288 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test Banner Components Banner Snapshot 1`] = ` + + + + + Banner title + + + + + + + + Action text + + + + + + + + + + + + + + + + +`; diff --git a/src/components/banner/__test__/banner.test.tsx b/src/components/banner/__test__/banner.test.tsx new file mode 100644 index 00000000..2c852960 --- /dev/null +++ b/src/components/banner/__test__/banner.test.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Alert, View } from "react-native"; +import * as TestRenderer from "react-test-renderer"; +import { Banner } from "../Banner"; + +const viewRef = React.createRef(); +const onLinkPress = () => { + Alert.alert("Alert", "Action triggered"); +}; + +describe("Test Banner Components", () => { + it("Banner Snapshot", () => { + const advice = TestRenderer.create().toJSON(); + expect(advice).toMatchSnapshot(); + }); +}); diff --git a/src/components/banner/index.tsx b/src/components/banner/index.tsx new file mode 100644 index 00000000..a5c72308 --- /dev/null +++ b/src/components/banner/index.tsx @@ -0,0 +1 @@ +export * from "./Banner"; \ No newline at end of file diff --git a/src/components/buttons/IconButton.tsx b/src/components/buttons/IconButton.tsx new file mode 100644 index 00000000..1e022bce --- /dev/null +++ b/src/components/buttons/IconButton.tsx @@ -0,0 +1,164 @@ +import React, { useCallback } from "react"; +import { GestureResponderEvent, Pressable } from "react-native"; +import Animated, { + Extrapolate, + interpolate, + interpolateColor, + useAnimatedProps, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withSpring +} from "react-native-reanimated"; +import { IOButtonStyles, IOColors, IOIconButtonStyles, IOScaleValues, IOSpringValues, hexToRgba } from "../../core"; +import { WithTestID } from "../../utils/types"; +import { AnimatedIcon, IOIcons, IconClassComponent } from "../icons"; + +export type IconButton = WithTestID<{ + color?: "primary" | "neutral" | "contrast"; + icon: IOIcons; + disabled?: boolean; + accessibilityLabel: string; + accessibilityHint?: string; + onPress: (event: GestureResponderEvent) => void; +}>; + +type ColorStates = { + icon: { + default: string; + pressed: string; + disabled: string; + }; +}; + +const mapColorStates: Record, ColorStates> = { + // Primary button + primary: { + icon: { + default: IOColors["blueIO-500"], + pressed: IOColors["blueIO-600"], + disabled: hexToRgba(IOColors["blueIO-500"], 0.25) + } + }, + // Neutral button + neutral: { + icon: { + default: IOColors.black, + pressed: IOColors["grey-850"], + disabled: IOColors.grey + } + }, + // Contrast button + contrast: { + icon: { + default: IOColors.white, + pressed: hexToRgba(IOColors.white, 0.85), + disabled: hexToRgba(IOColors.white, 0.25) + } + } +}; + +const AnimatedIconClassComponent = + Animated.createAnimatedComponent(IconClassComponent); + +export const IconButton = ({ + color = "primary", + icon, + disabled = false, + onPress, + accessibilityLabel, + accessibilityHint, + testID +}: IconButton) => { + const isPressed = useSharedValue(0); + + // Scaling transformation applied when the button is pressed + const animationScaleValue = IOScaleValues?.exaggeratedButton?.pressedState; + + // Using a spring-based animation for our interpolations + const progressPressed = useDerivedValue(() => + withSpring(isPressed.value, IOSpringValues.button) + ); + + // Interpolate animation values from `isPressed` values + + const pressedAnimationStyle = useAnimatedStyle(() => { + // Scale down button slightly when pressed + const scale = interpolate( + progressPressed.value, + [0, 1], + [1, animationScaleValue], + Extrapolate.CLAMP + ); + + return { + transform: [{ scale }] + }; + }); + + // Animate the color prop + const animatedColor = useAnimatedProps(() => { + const iconColor = interpolateColor( + progressPressed.value, + [0, 1], + [ + mapColorStates[color].icon.default, + mapColorStates[color].icon.pressed + ] + ); + return { color: iconColor }; + }); + /* REMOVE_LEGACY_COMPONENT: End ▶ */ + + const handlePressIn = useCallback(() => { + // eslint-disable-next-line functional/immutable-data + isPressed.value = 1; + }, [isPressed]); + const handlePressOut = useCallback(() => { + // eslint-disable-next-line functional/immutable-data + isPressed.value = 0; + }, [isPressed]); + + return ( + + + {!disabled ? ( + + ) : ( + + )} + + + ); +}; + +export default IconButton; diff --git a/src/components/buttons/index.tsx b/src/components/buttons/index.tsx index 4fd81589..e1ab8575 100644 --- a/src/components/buttons/index.tsx +++ b/src/components/buttons/index.tsx @@ -2,3 +2,4 @@ export * from "./ButtonSolid"; export * from "./ButtonLink"; export * from "./ButtonOutline"; export * from "./ButtonExtendedOutline"; +export * from "./IconButton"; \ No newline at end of file diff --git a/src/components/typography/H6.tsx b/src/components/typography/H6.tsx index 55736585..251487b1 100644 --- a/src/components/typography/H6.tsx +++ b/src/components/typography/H6.tsx @@ -5,8 +5,8 @@ import { useTypographyFactory } from "./Factory"; import { ExternalTypographyProps, TypographyProps } from "./common"; // when the weight is bold, only these color are allowed -type AllowedColors = IOTheme["textBody-default"]; -type AllowedWeight = Extract; +type AllowedColors = IOTheme["textBody-default"] | "blueIO-850"; +type AllowedWeight = Extract; type OwnProps = ExternalTypographyProps< TypographyProps