Skip to content

Commit

Permalink
[IOPLT-171] Introduces CodeInput component (#143)
Browse files Browse the repository at this point in the history
## Short description
This PR introduces `CodeInput` component

## How to test
Check NumberPad screen to view new `CodeInput` component features


https://github.com/pagopa/io-app-design-system/assets/3959405/ae9893d9-1629-4f1f-b0cf-5806dd74a9e3

---------

Co-authored-by: Damiano Plebani <[email protected]>
  • Loading branch information
CrisTofani and dmnplb authored Nov 23, 2023
1 parent 1c3f287 commit 0ac642b
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 23 deletions.
71 changes: 57 additions & 14 deletions example/src/pages/NumberPad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,81 @@ import {
IOStyles,
VSpacer,
NumberPad,
H3
H3,
CodeInput,
ListItemSwitch,
IOColors
} from "@pagopa/io-app-design-system";
import { useNavigation } from "@react-navigation/native";
import { Screen } from "../components/Screen";

const PIN_LENGTH = 6;
/**
* This Screen is used to test components in isolation while developing.
* @returns a screen with a flexed view where you can test components
*/
export const NumberPadScreen = () => {
const [value, setValue] = React.useState("");
const [blueBackground, setBlueBackground] = React.useState(false);

const navigation = useNavigation();

const onValueChange = (v: string) => {
if (v.length <= PIN_LENGTH) {
setValue(v);
}
};

React.useEffect(() => {
navigation.setOptions({
headerStyle: {
backgroundColor: blueBackground
? IOColors["blueIO-500"]
: IOColors.white
}
});
}, [blueBackground, navigation]);
return (
<Screen>
<View
style={[
IOStyles.flex,
{ paddingTop: IOVisualCostants.appMarginDefault }
]}
>
<H1>NumberPad</H1>
<View
style={[
IOStyles.flex,
{ paddingVertical: IOVisualCostants.appMarginDefault },
{
backgroundColor: blueBackground
? IOColors["blueIO-500"]
: IOColors.white
}
]}
>
<Screen>
<ListItemSwitch
label="Attiva sfondo blu"
value={blueBackground}
onSwitchValueChange={() => setBlueBackground(v => !v)}
/>
<H1>NumberPad + Code Input</H1>
<H5>{"Value Typed on the NumberPad component"}</H5>
<VSpacer />
<H3>{value}</H3>
<H3 color={blueBackground ? "white" : "black"}>{value}</H3>
<VSpacer />
<CodeInput
value={value}
length={PIN_LENGTH}
variant={blueBackground ? "light" : "dark"}
onValueChange={onValueChange}
onValidate={v => v === "123456"}
/>
<VSpacer size={48} />
<NumberPad
value={value}
deleteAccessibilityLabel="Delete"
onValueChange={setValue}
variant="light"
onValueChange={onValueChange}
variant={blueBackground ? "dark" : "light"}
biometricType="FACE_ID"
biometricAccessibilityLabel="Face ID"
onBiometricPress={() => Alert.alert("biometric")}
/>
</View>
</Screen>
</Screen>
</View>
);
};
8 changes: 7 additions & 1 deletion example/src/pages/Sandbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import { Screen } from "../components/Screen";
export const Sandbox = () => (
<Screen>
<View
style={[IOStyles.flex, { paddingTop: IOVisualCostants.appMarginDefault }]}
style={[
IOStyles.flex,
{
paddingTop: IOVisualCostants.appMarginDefault
// backgroundColor: IOColors["blueIO-500"]
}
]}
>
<H1>Sandbox</H1>
<H5>{"Insert here the component you're willing to test"}</H5>
Expand Down
121 changes: 121 additions & 0 deletions src/components/codeInput/CodeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useEffect, useMemo } from "react";
import { StyleSheet, View } from "react-native";
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withSequence,
withTiming
} from "react-native-reanimated";
import { IOColors, IOStyles } from "../../core";
import { triggerHaptic } from "../../functions";

type CodeInputProps = {
value: string;
onValueChange: (value: string) => void;
length: number;
onValidate: (value: string) => boolean;
variant?: "light" | "dark";
};

const DOT_SIZE = 16;

const styles = StyleSheet.create({
dotShape: {
width: DOT_SIZE,
height: DOT_SIZE,
borderRadius: 8,
borderWidth: 2
},
dotEmpty: {
borderColor: IOColors["grey-200"]
},
wrapper: { justifyContent: "center", gap: DOT_SIZE }
});

const EmptyDot = () => <View style={[styles.dotShape, styles.dotEmpty]} />;

const FilletDot = ({ color }: { color: IOColors }) => (
<View
style={[
styles.dotShape,
{ backgroundColor: IOColors[color], borderColor: IOColors[color] }
]}
/>
);

export const CodeInput = ({
length,
value,
onValueChange,
variant = "light",
onValidate
}: CodeInputProps) => {
const [status, setStatus] = React.useState<"default" | "error">("default");

const translate = useSharedValue(0);
const shakeOffset: number = 8;

const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translate.value }]
}));

const fillColor = useMemo(
() =>
status === "error"
? "error-600"
: variant === "light"
? "white"
: "black",
[variant, status]
);

useEffect(() => {
if (onValidate && value.length === length) {
const isValid = onValidate(value);

if (!isValid) {
setStatus("error");
triggerHaptic("notificationError");

// eslint-disable-next-line functional/immutable-data
translate.value = withSequence(
withTiming(shakeOffset, {
duration: 75,
easing: Easing.inOut(Easing.cubic)
}),
withTiming(-shakeOffset, {
duration: 75,
easing: Easing.inOut(Easing.cubic)
}),
withTiming(shakeOffset / 2, {
duration: 75,
easing: Easing.inOut(Easing.cubic)
}),
withTiming(-shakeOffset / 2, {
duration: 75,
easing: Easing.inOut(Easing.cubic)
}),
withTiming(0, { duration: 75, easing: Easing.inOut(Easing.cubic) })
);

const timer = setTimeout(() => {
setStatus("default");
onValueChange("");
}, 500);
return () => clearTimeout(timer);
}
}
return;
}, [value, onValidate, length, onValueChange, translate]);

return (
<Animated.View style={[IOStyles.row, styles.wrapper, animatedStyle]}>
{[...Array(length)].map((_, i) => (
<React.Fragment key={i}>
{value[i] ? <FilletDot color={fillColor} /> : <EmptyDot />}
</React.Fragment>
))}
</Animated.View>
);
};
1 change: 1 addition & 0 deletions src/components/codeInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./CodeInput";
1 change: 1 addition & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./banner";
export * from "./buttons";
export * from "./checkbox";
export * from "./contentWrapper";
export * from "./codeInput";
export * from "./divider";
export * from "./featureInfo";
export * from "./icons";
Expand Down
15 changes: 7 additions & 8 deletions src/components/numberpad/NumberPad.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable functional/immutable-data */
import React, { ComponentProps, useRef } from "react";
import React, { ComponentProps } from "react";
import { View } from "react-native";
import { BiometricsValidType } from "../../utils/types";
import { IONumberPadButtonStyles, IOStyles } from "../../core";
Expand All @@ -21,6 +20,7 @@ type BiometricAuthProps =
};

type NumberPadProps = {
value: string;
onValueChange: (value: string) => void;
variant: ComponentProps<typeof NumberButton>["variant"];
deleteAccessibilityLabel: string;
Expand All @@ -47,23 +47,22 @@ const ButtonWrapper = ({ children }: { children: React.ReactNode }) => (
</View>
);
export const NumberPad = ({
value,
variant = "dark",
onValueChange,
biometricType,
onBiometricPress,
biometricAccessibilityLabel,
deleteAccessibilityLabel
}: NumberPadProps) => {
const numberPadValue = useRef<string>("");

const numberPadPress = (number: number) => {
numberPadValue.current = `${numberPadValue.current}${number}`;
onValueChange(numberPadValue.current);
const newValue = `${value}${number}`;
onValueChange(newValue);
};

const onDeletePress = () => {
numberPadValue.current = numberPadValue.current.slice(0, -1);
onValueChange(numberPadValue.current);
const newValue = value.slice(0, -1);
onValueChange(newValue);
};

type ButtonType = "biometric" | "delete";
Expand Down

0 comments on commit 0ac642b

Please sign in to comment.