From 6ac47c1d9c60916be738888ff4c756bcf0164374 Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Thu, 2 Nov 2023 15:16:31 +0100 Subject: [PATCH 1/7] base implementation of numberPad --- example/src/pages/Sandbox.tsx | 13 ++- src/components/buttons/IconButton.tsx | 19 +++- src/components/index.tsx | 1 + src/components/numberpad/NumberButton.tsx | 113 +++++++++++++++++++ src/components/numberpad/NumberPad.tsx | 131 ++++++++++++++++++++++ src/components/numberpad/index.tsx | 2 + src/core/IOColors.ts | 1 + src/core/IOStyles.ts | 18 +++ src/utils/types.ts | 4 + 9 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 src/components/numberpad/NumberButton.tsx create mode 100644 src/components/numberpad/NumberPad.tsx create mode 100644 src/components/numberpad/index.tsx diff --git a/example/src/pages/Sandbox.tsx b/example/src/pages/Sandbox.tsx index 4dcefd6d..b4077c45 100644 --- a/example/src/pages/Sandbox.tsx +++ b/example/src/pages/Sandbox.tsx @@ -1,12 +1,14 @@ import * as React from "react"; -import { View } from "react-native"; +import { Alert, View } from "react-native"; import { H1, H5, IOVisualCostants, IOStyles, - VSpacer + VSpacer, + NumberPad } from "@pagopa/io-app-design-system"; +import { constNull } from "fp-ts/lib/function"; import { Screen } from "../components/Screen"; /** @@ -22,6 +24,13 @@ export const Sandbox = () => (
{"Insert here the component you're willing to test"}
{/* Insert here the component you're willing to test */} + Alert.alert("biometric")} + /> ); diff --git a/src/components/buttons/IconButton.tsx b/src/components/buttons/IconButton.tsx index 85379086..e056f2b4 100644 --- a/src/components/buttons/IconButton.tsx +++ b/src/components/buttons/IconButton.tsx @@ -15,15 +15,22 @@ import { IOIconButtonStyles, IOScaleValues, IOSpringValues, + IOStyles, hexToRgba, useIOExperimentalDesign } from "../../core"; import { WithTestID } from "../../utils/types"; -import { AnimatedIcon, IOIcons, IconClassComponent } from "../icons"; +import { + AnimatedIcon, + IOIconSizeScale, + IOIcons, + IconClassComponent +} from "../icons"; export type IconButton = WithTestID<{ color?: "primary" | "neutral" | "contrast"; icon: IOIcons; + iconSize?: IOIconSizeScale; disabled?: boolean; accessibilityLabel: string; accessibilityHint?: string; @@ -102,6 +109,7 @@ const AnimatedIconClassComponent = export const IconButton = ({ color = "primary", icon, + iconSize = 24, disabled = false, onPress, accessibilityLabel, @@ -181,17 +189,24 @@ export const IconButton = ({ {!disabled ? ( ) : ( - + )} diff --git a/src/components/index.tsx b/src/components/index.tsx index 267c2c81..23bfcb4f 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -23,3 +23,4 @@ export * from "./toast"; export * from "./typography"; export * from "./textInput"; export * from "./layout"; +export * from "./numberpad"; diff --git a/src/components/numberpad/NumberButton.tsx b/src/components/numberpad/NumberButton.tsx new file mode 100644 index 00000000..67e07c9d --- /dev/null +++ b/src/components/numberpad/NumberButton.tsx @@ -0,0 +1,113 @@ +import React, { useCallback, useMemo } from "react"; +import { Pressable } from "react-native"; +import Animated, { + Extrapolate, + interpolate, + interpolateColor, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withSpring +} from "react-native-reanimated"; +import { + IOColors, + IONumberPadButtonStyles, + IOScaleValues, + IOSpringValues +} from "../../core"; +import { H3 } from "../typography"; + +type NumberButtonVariantType = "light" | "dark"; + +type NumberButtonProps = { + variant: NumberButtonVariantType; + number: number; + onPress: (number: number) => void; +}; + +type ColorMapVariant = { + background: IOColors; + pressed: IOColors; + foreground: IOColors; +}; + +const colorMap: Record = { + light: { + background: "grey-50", + pressed: "grey-200", + foreground: "blueIO-500" + }, + dark: { + background: "blueIO-400", + pressed: "blueIO-200", + foreground: "white" + } +}; + +export const NumberButton = ({ + number, + variant, + onPress +}: NumberButtonProps) => { + const colors = useMemo(() => colorMap[variant], [variant]); + const isPressed = useSharedValue(0); + // Scaling transformation applied when the button is pressed + const animationScaleValue = IOScaleValues?.basicButton?.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 + const bgColor = interpolateColor( + progressPressed.value, + [0, 1], + [IOColors[colors.background], IOColors[colors.pressed]] + ); + + // Scale down button slightly when pressed + const scale = interpolate( + progressPressed.value, + [0, 1], + [1, animationScaleValue], + Extrapolate.CLAMP + ); + + return { + backgroundColor: bgColor, + 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]); + + return ( + onPress(number)} + > + +

{number}

+
+
+ ); +}; diff --git a/src/components/numberpad/NumberPad.tsx b/src/components/numberpad/NumberPad.tsx new file mode 100644 index 00000000..381f46ba --- /dev/null +++ b/src/components/numberpad/NumberPad.tsx @@ -0,0 +1,131 @@ +/* eslint-disable functional/immutable-data */ +import React, { ComponentProps, useRef } from "react"; +import { StyleSheet, View } from "react-native"; +import { BiometricsValidType } from "../../utils/types"; +import { IOStyles } from "../../core"; +import { VSpacer } from "../spacer"; +import { IconButton } from "../buttons"; +import { IOIconSizeScale, IOIcons } from "../icons"; +import { NumberButton } from "./NumberButton"; + +type BiometricAuthProps = + | { + biometricType: BiometricsValidType; + onBiometricPress: () => void; + biometricAccessibilityLabel: string; + } + | { + biometricType?: never; + onBiometricPress?: never; + biometricAccessibilityLabel?: never; + }; + +type NumberPadProps = { + onValueChange: (value: string) => void; + variant: ComponentProps["variant"]; +} & BiometricAuthProps; + +const styles = StyleSheet.create({ + lastRow: { + alignItems: "center", + justifyContent: "space-between" + }, + elementGap: { width: 56, height: 56 } +}); + +const mapIconSpecByBiometric: Record< + BiometricsValidType, + { icon: IOIcons; size: IOIconSizeScale } +> = { + FACE_ID: { icon: "biomFaceID", size: 32 }, + TOUCH_ID: { icon: "fingerprint", size: 24 }, + BIOMETRICS: { icon: "fingerprint", size: 24 } +}; + +const ButtonWrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); +export const NumberPad = ({ + variant = "dark", + onValueChange, + biometricType, + onBiometricPress, + biometricAccessibilityLabel +}: NumberPadProps) => { + const numberPadValue = useRef(""); + + const numberPadPress = (number: number) => { + numberPadValue.current = `${numberPadValue.current}${number}`; + onValueChange(numberPadValue.current); + }; + + const onDeletePress = () => { + numberPadValue.current = numberPadValue.current.slice(0, -1); + onValueChange(numberPadValue.current); + }; + + type ButtonType = "biometric" | "delete"; + + const RowButtons = ({ + buttons + }: { + buttons: ReadonlyArray; + }) => ( + + {buttons.map(elem => { + if (typeof elem === "number") { + return ( + + ); + } + + if (elem === "delete") { + return ( + + + + ); + } + return biometricType ? ( + + + + ) : ( + + ); + })} + + ); + + return ( + + + + + + + + + + ); +}; diff --git a/src/components/numberpad/index.tsx b/src/components/numberpad/index.tsx new file mode 100644 index 00000000..03d800ac --- /dev/null +++ b/src/components/numberpad/index.tsx @@ -0,0 +1,2 @@ +// export * from "./NumberButton"; +export * from "./NumberPad"; diff --git a/src/core/IOColors.ts b/src/core/IOColors.ts index 1ed8cdc6..42081d3b 100644 --- a/src/core/IOColors.ts +++ b/src/core/IOColors.ts @@ -40,6 +40,7 @@ export const IOColors = asIOColors({ "blueIO-850": "#031344", "blueIO-600": "#0932B6", "blueIO-500": "#0B3EE3", + "blueIO-400": "#3C65E9", "blueIO-450": "#2351E6" /* Dark mode */, "blueIO-200": "#9DB2F4", "blueIO-150": "#B6C5F7", diff --git a/src/core/IOStyles.ts b/src/core/IOStyles.ts index 97ade0c3..19a0b499 100644 --- a/src/core/IOStyles.ts +++ b/src/core/IOStyles.ts @@ -104,6 +104,8 @@ export const buttonSolidHeight: number = btnSizeDefault; // TODO: Replace the number type with the new IOIconSizeScale export const iconBtnSizeSmall: number = 24; +export const numberPadBtnSize: number = 56; + export const IOButtonLegacyStyles = StyleSheet.create({ /* BaseButton, used in the: ButtonSolid, ButtonOutline @@ -226,6 +228,22 @@ export const IOIconButtonStyles = StyleSheet.create({ } }); +export const IONumberPadButtonStyles = StyleSheet.create({ + /* IconButton */ + button: { + alignItems: "center", + justifyContent: "center" + }, + circularShape: { + // Circular shape + borderRadius: 100 + }, + buttonSize: { + width: numberPadBtnSize, + height: numberPadBtnSize + } +}); + /** * LIST ITEM STYLES */ diff --git a/src/utils/types.ts b/src/utils/types.ts index 525edd9d..56785fa2 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -30,3 +30,7 @@ export type XOR = T | U extends object : T | U; export type InputType = "credit-card" | "default"; + +// Biometrics type used in io-app code base +// https://github.com/pagopa/io-app/blob/master/ts/utils/biometrics.ts#L31 +export type BiometricsValidType = "BIOMETRICS" | "FACE_ID" | "TOUCH_ID"; From ae658dc87504d4374d41c1fd0d04ccdd4184e06b Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Thu, 2 Nov 2023 15:37:41 +0100 Subject: [PATCH 2/7] Adds component page on example app --- example/src/navigation/navigator.tsx | 9 ++++++ example/src/navigation/params.ts | 1 + example/src/navigation/routes.ts | 1 + example/src/pages/NumberPad.tsx | 43 ++++++++++++++++++++++++++++ example/src/pages/Sandbox.tsx | 13 ++------- 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 example/src/pages/NumberPad.tsx diff --git a/example/src/navigation/navigator.tsx b/example/src/navigation/navigator.tsx index 16ef6137..9dcaa72c 100644 --- a/example/src/navigation/navigator.tsx +++ b/example/src/navigation/navigator.tsx @@ -24,6 +24,7 @@ import { HeaderSecondLevelScreen } from "../pages/HeaderSecondLevel"; import { StaticHeaderSecondLevelScreen } from "../pages/StaticHeaderSecondLevel"; import { Toasts } from "../pages/Toasts"; import { HeaderFirstLevelScreen } from "../pages/HeaderFirstLevel"; +import { NumberPadScreen } from "../pages/NumberPad"; import { AppParamsList } from "./params"; import APP_ROUTES from "./routes"; @@ -97,6 +98,14 @@ const AppNavigator = () => ( headerBackTitleVisible: false }} /> + { + const [value, setValue] = React.useState(""); + return ( + + +

NumberPad

+
{"Value Typed on the NumberPad component"}
+ +

{value}

+ + Alert.alert("biometric")} + /> +
+
+ ); +}; diff --git a/example/src/pages/Sandbox.tsx b/example/src/pages/Sandbox.tsx index b4077c45..4dcefd6d 100644 --- a/example/src/pages/Sandbox.tsx +++ b/example/src/pages/Sandbox.tsx @@ -1,14 +1,12 @@ import * as React from "react"; -import { Alert, View } from "react-native"; +import { View } from "react-native"; import { H1, H5, IOVisualCostants, IOStyles, - VSpacer, - NumberPad + VSpacer } from "@pagopa/io-app-design-system"; -import { constNull } from "fp-ts/lib/function"; import { Screen } from "../components/Screen"; /** @@ -24,13 +22,6 @@ export const Sandbox = () => (
{"Insert here the component you're willing to test"}
{/* Insert here the component you're willing to test */} - Alert.alert("biometric")} - />
); From 71a88dc8d2aa24b3565c52f559e042b2b054c566 Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Thu, 2 Nov 2023 16:02:38 +0100 Subject: [PATCH 3/7] improvements and fixes --- src/components/numberpad/NumberPad.tsx | 34 ++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/components/numberpad/NumberPad.tsx b/src/components/numberpad/NumberPad.tsx index 381f46ba..c6405f27 100644 --- a/src/components/numberpad/NumberPad.tsx +++ b/src/components/numberpad/NumberPad.tsx @@ -1,8 +1,8 @@ /* eslint-disable functional/immutable-data */ import React, { ComponentProps, useRef } from "react"; -import { StyleSheet, View } from "react-native"; +import { View } from "react-native"; import { BiometricsValidType } from "../../utils/types"; -import { IOStyles } from "../../core"; +import { IONumberPadButtonStyles, IOStyles } from "../../core"; import { VSpacer } from "../spacer"; import { IconButton } from "../buttons"; import { IOIconSizeScale, IOIcons } from "../icons"; @@ -23,16 +23,9 @@ type BiometricAuthProps = type NumberPadProps = { onValueChange: (value: string) => void; variant: ComponentProps["variant"]; + deleteAccessibilityLabel: string; } & BiometricAuthProps; -const styles = StyleSheet.create({ - lastRow: { - alignItems: "center", - justifyContent: "space-between" - }, - elementGap: { width: 56, height: 56 } -}); - const mapIconSpecByBiometric: Record< BiometricsValidType, { icon: IOIcons; size: IOIconSizeScale } @@ -44,7 +37,11 @@ const mapIconSpecByBiometric: Record< const ButtonWrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -54,7 +51,8 @@ export const NumberPad = ({ onValueChange, biometricType, onBiometricPress, - biometricAccessibilityLabel + biometricAccessibilityLabel, + deleteAccessibilityLabel }: NumberPadProps) => { const numberPadValue = useRef(""); @@ -75,7 +73,13 @@ export const NumberPad = ({ }: { buttons: ReadonlyArray; }) => ( - + {buttons.map(elem => { if (typeof elem === "number") { return ( @@ -95,7 +99,7 @@ export const NumberPad = ({ icon="cancel" color={variant === "dark" ? "contrast" : "primary"} onPress={onDeletePress} - accessibilityLabel="Delete" + accessibilityLabel={deleteAccessibilityLabel} /> ); @@ -111,7 +115,7 @@ export const NumberPad = ({ /> ) : ( - + ); })} From d814d03e7fc9ad9114bb8289c84edd83721c3fa9 Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Thu, 2 Nov 2023 16:07:57 +0100 Subject: [PATCH 4/7] fixes --- example/src/pages/NumberPad.tsx | 1 + src/components/numberpad/NumberPad.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/pages/NumberPad.tsx b/example/src/pages/NumberPad.tsx index 144aa0ea..36d3a9dd 100644 --- a/example/src/pages/NumberPad.tsx +++ b/example/src/pages/NumberPad.tsx @@ -31,6 +31,7 @@ export const NumberPadScreen = () => {

{value}

{buttons.map(elem => { From 003dbb120131f825ed854e1c9c384c098cdee25d Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Thu, 2 Nov 2023 16:18:06 +0100 Subject: [PATCH 5/7] fix compilation errors --- example/src/pages/MainScreen.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/pages/MainScreen.tsx b/example/src/pages/MainScreen.tsx index f6043476..494cbb91 100644 --- a/example/src/pages/MainScreen.tsx +++ b/example/src/pages/MainScreen.tsx @@ -65,7 +65,8 @@ const MainScreen = (props: Props) => { props.navigation.navigate(route as keyof AppParamsList)} + // we're using as any cause of compilation error + onPress={() => props.navigation.navigate(route as any)} /> ); From 3bd5bba70270f485f0498cbba942cd142320d55c Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Fri, 3 Nov 2023 14:09:47 +0100 Subject: [PATCH 6/7] updates icon button snap --- .../buttons/__test__/__snapshots__/button.test.tsx.snap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/buttons/__test__/__snapshots__/button.test.tsx.snap b/src/components/buttons/__test__/__snapshots__/button.test.tsx.snap index b1e6024a..156a64a8 100644 --- a/src/components/buttons/__test__/__snapshots__/button.test.tsx.snap +++ b/src/components/buttons/__test__/__snapshots__/button.test.tsx.snap @@ -532,6 +532,12 @@ exports[`Test Buttons Components IconButton Snapshot 1`] = ` "height": 24, "width": 24, }, + { + "alignItems": "center", + }, + { + "justifyContent": "center", + }, { "transform": [ { From d684d678848fc1a8f8350871aad76d188e0b263f Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Mon, 6 Nov 2023 15:28:21 +0100 Subject: [PATCH 7/7] adds the legacy variant for colorMap specs --- src/components/numberpad/NumberButton.tsx | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/numberpad/NumberButton.tsx b/src/components/numberpad/NumberButton.tsx index 67e07c9d..669f53ba 100644 --- a/src/components/numberpad/NumberButton.tsx +++ b/src/components/numberpad/NumberButton.tsx @@ -13,7 +13,8 @@ import { IOColors, IONumberPadButtonStyles, IOScaleValues, - IOSpringValues + IOSpringValues, + useIOExperimentalDesign } from "../../core"; import { H3 } from "../typography"; @@ -44,12 +45,30 @@ const colorMap: Record = { } }; +const legacyColorMap: Record = { + light: { + background: "grey-50", + pressed: "grey-200", + foreground: "blue" + }, + dark: { + background: "blue", + pressed: "blue-600", + foreground: "white" + } +}; + export const NumberButton = ({ number, variant, onPress }: NumberButtonProps) => { - const colors = useMemo(() => colorMap[variant], [variant]); + const { isExperimental } = useIOExperimentalDesign(); + + const colors = useMemo( + () => (isExperimental ? colorMap[variant] : legacyColorMap[variant]), + [variant, isExperimental] + ); const isPressed = useSharedValue(0); // Scaling transformation applied when the button is pressed const animationScaleValue = IOScaleValues?.basicButton?.pressedState;