Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IOPID-1899] NumberPad optimization #284

Merged
merged 12 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions example/src/pages/NumberPad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
useIOTheme
} from "@pagopa/io-app-design-system";
import { useNavigation } from "@react-navigation/native";
import * as React from "react";
import React, { useCallback, useEffect, useState } from "react";
import { Alert, View } from "react-native";

const PIN_LENGTH = 6;
Expand All @@ -25,16 +25,20 @@ export const NumberPadScreen = () => {
const theme = useIOTheme();
const navigation = useNavigation();

const [value, setValue] = React.useState("");
const [blueBackground, setBlueBackground] = React.useState(false);
const [value, setValue] = useState("");
const [blueBackground, setBlueBackground] = useState(false);

const onValueChange = (v: string) => {
if (v.length <= PIN_LENGTH) {
setValue(v);
}
};
const onNumberPress = useCallback((v: number) => {
setValue((prev) => prev.length < PIN_LENGTH ? `${prev}${v}` : prev);
}, []);

React.useEffect(() => {
const onDeletePress = useCallback(() => {
setValue((prev) => prev.slice(0, -1));
}, []);

const onBiometricPress = useCallback(() => Alert.alert("biometric"),[]);

useEffect(() => {
navigation.setOptions({
headerStyle: {
backgroundColor: blueBackground
Expand Down Expand Up @@ -74,18 +78,18 @@ export const NumberPadScreen = () => {
value={value}
length={PIN_LENGTH}
variant={blueBackground ? "light" : "dark"}
onValueChange={onValueChange}
onValueChange={setValue}
onValidate={v => v === "123456"}
/>
<VSpacer size={48} />
<NumberPad
value={value}
deleteAccessibilityLabel="Delete"
onValueChange={onValueChange}
onNumberPress={onNumberPress}
onDeletePress={onDeletePress}
variant={blueBackground ? "dark" : "light"}
biometricType="FACE_ID"
biometricAccessibilityLabel="Face ID"
onBiometricPress={() => Alert.alert("biometric")}
onBiometricPress={onBiometricPress}
/>
</ContentWrapper>
</View>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"prettify": "prettier --write \"src/**/*.(ts|tsx)\"",
"prepack": "bob build",
"prepare": "bob build",
"release": "release-it",
"example": "yarn --cwd example",
"bootstrap": "yarn example && yarn install",
Expand Down
33 changes: 26 additions & 7 deletions src/components/numberpad/NumberButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from "react";
import React, { memo, useCallback, useMemo } from "react";
import { Pressable } from "react-native";
import Animated, {
Extrapolate,
Expand All @@ -22,8 +22,19 @@ import { H3 } from "../typography";
type NumberButtonVariantType = "light" | "dark";

type NumberButtonProps = {
/**
* Used to choose the component color variant between `dark` and `light`.
*/
variant: NumberButtonVariantType;
/**
* The button value.
*/
number: number;
/**
* The action to be executed when the button is pressed.
* @param number
* @returns void
*/
onPress: (number: number) => void;
};

Expand Down Expand Up @@ -58,8 +69,12 @@ const legacyColorMap: Record<NumberButtonVariantType, ColorMapVariant> = {
foreground: "white"
}
};

export const NumberButton = ({
/**
* Based on a `Pressable` element, it displays a number button with animations on press In and Out.
*
* @returns {JSX.Element} The rendered `NumberButton`
*/
export const NumberButton = memo(({
number,
variant,
onPress
Expand Down Expand Up @@ -110,13 +125,17 @@ export const NumberButton = ({
isPressed.value = 0;
}, [isPressed]);

const handleOnPress = useCallback(() => {
onPress(number);
}, [number, onPress]);

return (
<Pressable
accessible={true}
accessibilityRole={"button"}
accessible
accessibilityRole="button"
onPressIn={onPressIn}
onPressOut={onPressOut}
onPress={() => onPress(number)}
onPress={handleOnPress}
>
<Animated.View
style={[
Expand All @@ -130,4 +149,4 @@ export const NumberButton = ({
</Animated.View>
</Pressable>
);
};
});
219 changes: 126 additions & 93 deletions src/components/numberpad/NumberPad.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ComponentProps } from "react";
import { View } from "react-native";
import { BiometricsValidType } from "../../utils/types";
import React, { ComponentProps, Fragment, useCallback, useMemo } from "react";
import { StyleSheet, View } from "react-native";
import { BiometricsValidType, Optional } from "../../utils/types";
import { IONumberPadButtonStyles, IOStyles } from "../../core";
import { VSpacer } from "../spacer";
import { IconButton } from "../buttons";
Expand All @@ -9,21 +9,49 @@ import { NumberButton } from "./NumberButton";

type BiometricAuthProps =
| {
biometricType: BiometricsValidType;
onBiometricPress: () => void;
biometricAccessibilityLabel: string;
}
/**
* Type of device biometric.
*/
biometricType: BiometricsValidType;
/**
* Function to be executed when the biometric button is pressed.
* @returns void
*/
onBiometricPress: () => void;
/**
* This label will be read from ScreenReaders and give informations about biometric button.
*/
biometricAccessibilityLabel: string;
}
| {
biometricType?: never;
onBiometricPress?: never;
biometricAccessibilityLabel?: never;
};
biometricType?: never;
onBiometricPress?: never;
biometricAccessibilityLabel?: never;
};

type NumberPadProps = {
value: string;
onValueChange: (value: string) => void;
variant: ComponentProps<typeof NumberButton>["variant"];
/**
* Used to choose the component color variant between `dark` and `light`.
*
* The default value is `dark`.
*/
variant?: ComponentProps<typeof NumberButton>["variant"];
/**
* This label will be read from ScreenReaders and give informations about delete button.
*/
deleteAccessibilityLabel: string;
/**
* This function is passed to all numeric buttons to handle their press action.
* @param value
* @returns void
*/
onNumberPress: (value: number) => void;
/**
* This function is passed to the delete button to handle the action to trigger when it's pressed.
*
* @returns void
*/
onDeletePress: () => void;
} & BiometricAuthProps;

const mapIconSpecByBiometric: Record<
Expand All @@ -35,79 +63,53 @@ const mapIconSpecByBiometric: Record<
BIOMETRICS: { icon: "fingerprint", size: 24 }
};

const ButtonWrapper = ({ children }: { children: React.ReactNode }) => (
<View
style={[
IONumberPadButtonStyles.buttonSize,
IOStyles.alignCenter,
IOStyles.centerJustified
]}
>
{children}
</View>
);
/**
* This component displays a custom numeric keyboard.
*
* It accepts an optional `biometricType` prop which enables an extra keyboard button that accepts a `onBiometricPress` prop used to handle the action to be executed when it's pressed.
* @returns {JSX.Element} The rendered numeric keyboard component.
*/
export const NumberPad = ({
value,
variant = "dark",
onValueChange,
biometricType,
onBiometricPress,
biometricAccessibilityLabel,
deleteAccessibilityLabel
}: NumberPadProps) => {
const numberPadPress = (number: number) => {
const newValue = `${value}${number}`;
onValueChange(newValue);
};

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

type ButtonType = "biometric" | "delete";
deleteAccessibilityLabel,
onNumberPress,
onBiometricPress,
onDeletePress
}: NumberPadProps): JSX.Element => {
/**
* Renders the buttons row from a given array.
*/
// eslint-disable-next-line arrow-body-style
const renderButtonsRow = useCallback((row: Array<Optional<number | string>>) => {
return row.map((item) => {
if (typeof item === "number") {
return (
<NumberButton
key={item}
number={item}
onPress={onNumberPress}
variant={variant}
/>
);
}

const RowButtons = ({
buttons
}: {
buttons: ReadonlyArray<number | ButtonType>;
}) => (
<View
style={[
IOStyles.rowSpaceBetween,
{
justifyContent: "space-between",
alignItems: "center",
flexGrow: 1
}
]}
>
{buttons.map(elem => {
if (typeof elem === "number") {
return (
<NumberButton
key={elem}
number={elem}
onPress={numberPadPress}
variant={variant}
if (item === "delete") {
return (
<ButtonWrapper key={item}>
<IconButton
icon="cancel"
color={variant === "dark" ? "contrast" : "primary"}
onPress={onDeletePress}
accessibilityLabel={deleteAccessibilityLabel}
/>
);
}

if (elem === "delete") {
return (
<ButtonWrapper key={elem}>
<IconButton
icon="cancel"
color={variant === "dark" ? "contrast" : "primary"}
onPress={onDeletePress}
accessibilityLabel={deleteAccessibilityLabel}
/>
</ButtonWrapper>
);
}
return biometricType ? (
<ButtonWrapper key={elem}>
</ButtonWrapper>
);
}
if (biometricType && mapIconSpecByBiometric[biometricType]) {
return (
<ButtonWrapper key={item}>
<IconButton
icon={mapIconSpecByBiometric[biometricType].icon}
iconSize={mapIconSpecByBiometric[biometricType].size}
Expand All @@ -116,22 +118,53 @@ export const NumberPad = ({
accessibilityLabel={biometricAccessibilityLabel}
/>
</ButtonWrapper>
) : (
<View key={"emptyElem"} style={IONumberPadButtonStyles.buttonSize} />
);
})}
</View>
);
}

return <View key={"emptyElem"} style={IONumberPadButtonStyles.buttonSize} />;
});
}, [biometricAccessibilityLabel, biometricType, deleteAccessibilityLabel, onBiometricPress, onDeletePress, onNumberPress, variant]);

// eslint-disable-next-line arrow-body-style
const numberPad = useMemo(() => {
return [[1, 2, 3], [4, 5, 6], [7, 8, 9], [biometricType, 0, 'delete']].map((row, i, self) =>
<Fragment key={i}>
<View
style={[
IOStyles.rowSpaceBetween,
styles.numberPad
]}
>
{renderButtonsRow(row)}
</View>
{i < self.length - 1 && <VSpacer />}
</Fragment>
);
}, [biometricType, renderButtonsRow]);

return (
<View style={IOStyles.horizontalContentPadding}>
<RowButtons buttons={[1, 2, 3]} />
<VSpacer />
<RowButtons buttons={[4, 5, 6]} />
<VSpacer />
<RowButtons buttons={[7, 8, 9]} />
<VSpacer />
<RowButtons buttons={["biometric", 0, "delete"]} />
{numberPad}
</View>
);
};

const ButtonWrapper = ({ children }: { children: React.ReactNode }) => (
<View
style={[
IONumberPadButtonStyles.buttonSize,
IOStyles.alignCenter,
IOStyles.centerJustified
]}
>
{children}
</View>
);

const styles = StyleSheet.create({
numberPad: {
justifyContent: "space-between",
alignItems: "center",
flexGrow: 1
}
});
Loading
Loading