teamdigitale/italia-app

View on GitHub
ts/components/LabelledItem/index.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * Display a labelled, followed by a
 * input and an icon on the left-end
 * side of the input
 *
 * LABEL
 * X __________
 * ^     ^
 * icon  |
 *       input
 */
import { HSpacer, IOColors, IOIcons } from "@pagopa/io-app-design-system";
import { useFocusEffect } from "@react-navigation/native";
import color from "color";
import * as React from "react";
import { useState } from "react";
import {
  ImageSourcePropType,
  ImageStyle,
  NativeSyntheticEvent,
  StyleSheet,
  TextInput,
  TextInputFocusEventData,
  TextInputProps,
  View
} from "react-native";
import { TextInputMaskProps } from "react-native-masked-text";
import I18n from "../../i18n";
import { WithTestID } from "../../types/WithTestID";

import { isStringNullyOrEmpty } from "../../utils/strings";
import { makeFontStyleObject } from "../core/fonts";
import { H5 } from "../core/typography/H5";
import { IOStyles } from "../core/variables/IOStyles";
import TextInputMask from "../ui/MaskedInput";
import variables from "../../theme/variables";
import { LabelledItemIconOrImage } from "./LabelledItemIconOrImage";

const styles = StyleSheet.create({
  bottomLine: {
    borderBottomWidth: 1
  },
  textInputMask: {
    height: variables.inputHeightBase,
    color: IOColors["grey-850"],
    paddingLeft: 5,
    paddingRight: 5,
    flex: 1,
    fontSize: variables.inputFontSize,
    ...makeFontStyleObject("Regular")
  },
  regularInput: {
    flexGrow: 1,
    paddingVertical: 8,
    ...makeFontStyleObject("Regular")
  }
});

interface TextInputAdditionalProps extends TextInputProps {
  disabled?: boolean;
}

type CommonProp = Readonly<{
  accessibilityHint?: string;
  accessibilityLabel?: string;
  accessibilityLabelIcon?: string;
  description?: string;
  focusBorderColor?: string;
  overrideBorderColor?: string;
  hasNavigationEvents?: boolean;
  icon?: IOIcons | ImageSourcePropType;
  iconColor?: IOColors;
  imageStyle?: ImageStyle;
  iconPosition?: "left" | "right";
  inputMaskProps?: TextInputMaskProps &
    React.ComponentPropsWithRef<typeof TextInputMask>;
  inputProps?: TextInputAdditionalProps;
  isValid?: boolean;
  label?: string;
  onPress?: () => void;
  inputAccessoryViewID?: string;
}>;

export type Props = WithTestID<CommonProp>;

/* TODO: Replace this generated color variable with a value from IOCOlors
Or Alias Token from variables.ts */
const brandGrayDarken = color(IOColors.greyUltraLight).darken(0.2).string();

type DescriptionColor = "bluegreyLight" | "bluegreyDark" | "red";
type LabelColor = Exclude<DescriptionColor, "red">;
type ColorByProps = {
  borderColor: string | undefined;
  descriptionColor: DescriptionColor;
  iconColor: IOColors;
  labelColor: LabelColor;
  placeholderTextColor: string;
};
function getColorsByProps({
  isDisabledTextInput,
  hasFocus,
  isEmpty,
  isValid,
  iconColor
}: {
  isDisabledTextInput: boolean;
  hasFocus: boolean;
  isEmpty: boolean;
  isValid?: boolean;
  iconColor?: IOColors;
}): ColorByProps {
  if (isDisabledTextInput) {
    return {
      borderColor: IOColors.greyLight,
      descriptionColor: "bluegreyLight",
      iconColor: iconColor ?? "bluegreyLight",
      labelColor: "bluegreyLight",
      placeholderTextColor: IOColors.bluegreyLight
    };
  }
  return {
    borderColor: hasFocus && isEmpty ? IOColors.bluegrey : undefined,
    descriptionColor: isValid === false ? "red" : "bluegreyDark",
    iconColor: iconColor ?? "bluegrey",
    placeholderTextColor: brandGrayDarken,
    labelColor: "bluegreyDark"
  };
}

const NavigationEventHandler = ({ onPress }: { onPress: () => void }) => {
  useFocusEffect(React.useCallback(() => onPress, [onPress]));

  return <></>;
};

export const LabelledItem: React.FC<Props> = ({
  iconPosition = "left",
  ...props
}: Props) => {
  const [isEmpty, setIsEmpty] = useState(true);
  const [hasFocus, setHasFocus] = useState(false);
  const accessibilityLabel = props.accessibilityLabel ?? "";

  const {
    borderColor,
    descriptionColor,
    iconColor,
    placeholderTextColor,
    labelColor
  } = getColorsByProps({
    isDisabledTextInput: Boolean(props.inputProps && props.inputProps.disabled),
    hasFocus,
    isEmpty,
    isValid: props.isValid,
    iconColor: props.iconColor
  });

  const handleOnFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
    props.inputProps?.onFocus?.(e);
    setHasFocus(true);
  };

  const handleOnBlur = () => {
    setHasFocus(false);
  };

  /**
   * check if the input is empty and set the value in the state
   */
  const checkInputIsEmpty = (text?: string) =>
    setIsEmpty(isStringNullyOrEmpty(text));

  return (
    <View style={{ flexGrow: 1 }}>
      {props.label && (
        <View
          testID="label"
          importantForAccessibility="no-hide-descendants"
          accessibilityElementsHidden={true}
        >
          <H5 color={labelColor}>{props.label}</H5>
        </View>
      )}

      <View>
        <View
          style={{
            ...IOStyles.row,
            ...styles.bottomLine,
            borderColor: props.overrideBorderColor
              ? props.overrideBorderColor
              : borderColor || props.focusBorderColor
          }}
          testID="Item"
        >
          {props.hasNavigationEvents && props.onPress && (
            <NavigationEventHandler onPress={props.onPress} />
          )}

          {/* Icon OR Image. They can't be managed separately because
          credit card sorting have a fallback value that's an icon,
          not an image */}
          {iconPosition === "left" && props.icon && (
            <>
              <LabelledItemIconOrImage
                icon={props.icon}
                iconColor={iconColor}
                imageStyle={props.imageStyle}
                accessible={false}
                accessibilityLabelIcon={props.accessibilityLabelIcon}
                onPress={props.onPress}
              />
              <HSpacer size={8} />
            </>
          )}

          {props.inputMaskProps && (
            <TextInputMask
              accessible={true}
              accessibilityLabel={I18n.t("global.accessibility.textField", {
                inputLabel: accessibilityLabel
              })}
              accessibilityHint={props.accessibilityHint}
              underlineColorAndroid="transparent"
              style={styles.textInputMask}
              {...props.inputMaskProps}
              onChangeText={(formatted: string, text?: string) => {
                props.inputMaskProps?.onChangeText?.(formatted, text);
                checkInputIsEmpty(text);
              }}
              onFocus={handleOnFocus}
              onBlur={handleOnBlur}
              testID={`${props.testID}InputMask`}
              inputAccessoryViewID={props.inputAccessoryViewID}
            />
          )}

          {props.inputProps && (
            <TextInput
              accessible={true}
              accessibilityLabel={I18n.t("global.accessibility.textField", {
                inputLabel: accessibilityLabel
              })}
              accessibilityHint={props.accessibilityHint}
              underlineColorAndroid="transparent"
              {...props.inputProps}
              onChangeText={(text: string) => {
                props.inputProps?.onChangeText?.(text);
                checkInputIsEmpty(text);
              }}
              onFocus={handleOnFocus}
              onBlur={handleOnBlur}
              testID={`${props.testID}Input`}
              editable={props.inputProps?.disabled}
              placeholderTextColor={placeholderTextColor}
              inputAccessoryViewID={props.inputAccessoryViewID}
              style={styles.regularInput}
            />
          )}

          {iconPosition === "right" && props.icon && (
            <LabelledItemIconOrImage
              icon={props.icon}
              iconColor={iconColor}
              imageStyle={props.imageStyle}
              accessibilityLabelIcon={props.accessibilityLabelIcon}
              onPress={props.onPress}
            />
          )}
        </View>
      </View>
      {props.description && (
        <View
          testID="description"
          importantForAccessibility="no-hide-descendants"
          accessibilityElementsHidden={true}
          key={"description"}
        >
          <H5
            weight={"Regular"}
            color={descriptionColor}
            testID="H5-description"
          >
            {props.description}
          </H5>
        </View>
      )}
    </View>
  );
};