From 4af4f4be7d5a6ca4114c8e7c097a9b5d32777263 Mon Sep 17 00:00:00 2001 From: Alessandro Izzo <34343582+Hantex9@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:59:00 +0200 Subject: [PATCH] feat: [IOBP-186] Import updated `ListItemTransaction` component (#51) ## Short description This PR imports the new updated version of `ListItemTransaction` component and adds a new prop `badgeText` in order to show a text inside when the badge is visible (useful for translated text) ## List of changes proposed in this pull request - Added `ListItemTransaction` component and its utilities; - Added a new propr `badgeText` which is required only when the status of a transaction is: `"failure" | "pending" | "cancelled" | "reversal"` ## How to test Open the ListItem section and check if the ListItemTransaction is correctly showed; ## Preview https://github.com/pagopa/io-app-design-system/assets/34343582/e8a786ce-a229-482d-811e-3b7d7160076c --------- Co-authored-by: Damiano Plebani --- example/src/pages/ListItem.tsx | 173 ++++++++++-------- .../common/LogoPaymentWithFallback.tsx | 60 ++++++ .../listitems/ListItemTransaction.tsx | 121 ++++++++---- src/utils/accessibility.ts | 17 ++ src/utils/object.ts | 12 ++ src/utils/url.ts | 9 + 6 files changed, 273 insertions(+), 119 deletions(-) create mode 100644 src/components/common/LogoPaymentWithFallback.tsx create mode 100644 src/utils/accessibility.ts create mode 100644 src/utils/object.ts create mode 100644 src/utils/url.ts diff --git a/example/src/pages/ListItem.tsx b/example/src/pages/ListItem.tsx index eb72df76..aec3d446 100644 --- a/example/src/pages/ListItem.tsx +++ b/example/src/pages/ListItem.tsx @@ -2,6 +2,7 @@ import { ButtonLink, H2, IOThemeContext, + Icon, IconButton, ListItemAction, ListItemIDP, @@ -380,84 +381,94 @@ const renderListItemIDP = () => ( ); -const renderListItemTransaction = () => ( - - - - - - - - - - - } - transactionAmount="" - onPress={onButtonPress} - /> - } - transactionAmount="€ 100" - onPress={onButtonPress} - /> - - -); +const renderListItemTransaction = () => { + const cdnPath = "https://assets.cdn.io.italia.it/logos/organizations/"; + const organizationLogoURI = { + imageSource: `${cdnPath}82003830161.png`, + name: "Comune di Milano" + }; + return ( + + + + + + + + + + + } + transactionAmount="" + onPress={onButtonPress} + /> + } + transactionAmount="€ 100" + onPress={onButtonPress} + /> + + + ); +}; diff --git a/src/components/common/LogoPaymentWithFallback.tsx b/src/components/common/LogoPaymentWithFallback.tsx new file mode 100644 index 00000000..2dbfb6fd --- /dev/null +++ b/src/components/common/LogoPaymentWithFallback.tsx @@ -0,0 +1,60 @@ +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import * as React from "react"; +import { findFirstCaseInsensitive } from "../../utils/object"; +import { IOColors } from "../../core"; +import { IOIconSizeScale, Icon } from "../icons"; +import { + IOLogoPaymentExtType, + IOLogoPaymentType, + IOPaymentExtLogos, + IOPaymentLogos, + LogoPayment, + LogoPaymentExt +} from "../logos"; + +export type LogoPaymentWithFallback = { + brand?: string; + fallbackIconColor?: IOColors; + size?: IOIconSizeScale; + isExtended?: boolean; +}; +export type LogoPaymentExtOrDefaultIconProps = { + cardIcon?: IOLogoPaymentExtType; + fallbackIconColor?: IOColors; + size?: IOIconSizeScale; +}; +/** + * This component renders either + * - a LogoPayment/LogoPaymentExt component + * - a default credit card icon + * @param cardIcon: IOLogoPaymentType icon + * @param size: the size of the icon (standard is 24/48) + * @param fallbackIconColor: default icon color (standard is grey-700) + * @param isExtended: if true, renders a LogoPaymentExt component + * @returns a LogoPayment/LogopaymentExt component if the cardIcon is supported, a default credit card icon otherwise + */ +export const LogoPaymentWithFallback = ({ + brand, + fallbackIconColor = "grey-700", + isExtended = false, + size = isExtended ? 48 : 24 +}: LogoPaymentWithFallback) => { + const logos = isExtended ? IOPaymentExtLogos : IOPaymentLogos; + + return pipe( + brand, + O.fromNullable, + O.chain(findFirstCaseInsensitive(logos)), + O.map(([brand]) => brand), + O.fold( + () => , + brand => + isExtended ? ( + + ) : ( + + ) + ) + ); +}; diff --git a/src/components/listitems/ListItemTransaction.tsx b/src/components/listitems/ListItemTransaction.tsx index 2346c89e..244b8216 100644 --- a/src/components/listitems/ListItemTransaction.tsx +++ b/src/components/listitems/ListItemTransaction.tsx @@ -3,19 +3,25 @@ import { pipe } from "fp-ts/lib/function"; import React from "react"; import { ImageURISource, StyleSheet, View } from "react-native"; import Placeholder from "rn-placeholder"; + import { IOColors, + IOListItemLogoMargin, IOListItemStyles, IOListItemVisualParams, IOStyles, IOVisualCostants, useIOTheme } from "../../core"; + +import { LogoPaymentWithFallback } from "../common/LogoPaymentWithFallback"; +import { isImageUri } from "../../utils/url"; import { WithTestID } from "../../utils/types"; +import { getAccessibleAmountText } from "../../utils/accessibility"; import { Avatar } from "../avatar/Avatar"; import { Badge } from "../badge/Badge"; -import { Icon } from "../icons"; -import { IOLogoPaymentType, LogoPayment } from "../logos"; +import { IOIconSizeScale, Icon } from "../icons"; +import { IOLogoPaymentType } from "../logos"; import { VSpacer } from "../spacer"; import { H6, LabelSmall } from "../typography"; import { @@ -23,95 +29,135 @@ import { PressableListItemBase } from "./PressableListItemsBase"; -type LogoNameOrUri = IOLogoPaymentType | ImageURISource; +export type ListItemTransactionStatus = + | "success" + | "failure" + | "pending" + | "cancelled" + | "refunded" + | "reversal"; + +type PaymentLogoIcon = IOLogoPaymentType | ImageURISource | React.ReactNode; + export type ListItemTransaction = WithTestID< PressableBaseProps & { hasChevronRight?: boolean; isLoading?: boolean; - paymentLogoOrUrl?: LogoNameOrUri; + /** + * A logo that will be displayed on the left of the list item. + * + * Must be a {@link IOLogoPaymentType} or an {@link ImageURISource} or an {@link Icon}. + */ + paymentLogoIcon?: PaymentLogoIcon; subtitle: string; title: string; } & ( | { - transactionStatus: "success"; + transactionStatus: "success" | "refunded"; + badgeText?: string; transactionAmount: string; } | { - transactionStatus: "failure" | "pending"; + transactionStatus: "failure" | "pending" | "cancelled" | "reversal"; + badgeText: string; transactionAmount?: string; } ) >; type LeftComponentProps = { - logoNameOrUrl: LogoNameOrUri; + logoIcon: PaymentLogoIcon; }; -const isImageUrI = ( - value: IOLogoPaymentType | ImageURISource -): value is ImageURISource => - typeof value === "object" && value.uri !== undefined; +const CARD_LOGO_SIZE: IOIconSizeScale = 24; +const MUNICIPALITY_LOGO_SIZE = 44; +// this is the 's "small" size, +// since it is bigger than the card logos, we use +// it as a base size for homogeneous sizing via container size. -const LeftComponent = ({ logoNameOrUrl }: LeftComponentProps) => { - if (isImageUrI(logoNameOrUrl)) { - return ; - } else { - return ( - - - - ); +const LeftComponent = ({ logoIcon }: LeftComponentProps) => { + if (isImageUri(logoIcon)) { + return ; + } + if (React.isValidElement(logoIcon)) { + return <>{logoIcon}; } + return ( + + ); }; export const ListItemTransaction = ({ accessibilityLabel, hasChevronRight = false, isLoading = false, - paymentLogoOrUrl, + paymentLogoIcon, onPress, subtitle, testID, title, transactionAmount, + badgeText, transactionStatus = "success" }: ListItemTransaction) => { const theme = useIOTheme(); + const maybeBadgeText = pipe( + badgeText, + O.fromNullable, + O.getOrElse(() => "-") + ); + if (isLoading) { return ; } - const designSystemBlue: IOColors = "blue"; + const designSystemBlue: IOColors = "blueIO-500"; const ListItemTransactionContent = () => { const TransactionAmountOrBadgeComponent = () => { switch (transactionStatus) { case "success": return ( -
- {transactionAmount || "-"} +
+ {transactionAmount || ""} +
+ ); + case "refunded": + return ( +
+ {transactionAmount || ""}
); - case "failure": - return ; + case "cancelled": + return ; + case "reversal": + return ; case "pending": - return ; + return ; } }; return ( <> - {paymentLogoOrUrl && ( - - + {paymentLogoIcon && ( + + )} @@ -121,7 +167,6 @@ export const ListItemTransaction = ({ {subtitle} - diff --git a/src/utils/accessibility.ts b/src/utils/accessibility.ts new file mode 100644 index 00000000..31da6295 --- /dev/null +++ b/src/utils/accessibility.ts @@ -0,0 +1,17 @@ +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import I18n from "i18n-js"; + +/** + * This function is used to get the text that will be read by the screen reader + * with the correct minus symbol pronunciation. + */ +export const getAccessibleAmountText = (amount?: string) => + pipe( + amount, + O.fromNullable, + O.map(amount => + amount.replace("-", I18n.t("global.accessibility.minusSymbol")) + ), + O.getOrElseW(() => undefined) + ); diff --git a/src/utils/object.ts b/src/utils/object.ts new file mode 100644 index 00000000..7cf9fc84 --- /dev/null +++ b/src/utils/object.ts @@ -0,0 +1,12 @@ +import * as A from "fp-ts/Array"; +import * as O from "fp-ts/Option"; +import { pipe } from "fp-ts/lib/function"; + +export const findFirstCaseInsensitive = + (obj: { [key: string]: T }) => + (key: string): O.Option<[string, T]> => + pipe( + obj, + Object.entries, + A.findFirst(([k, _]) => k.toLowerCase() === key.toLowerCase()) + ); diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 00000000..b92424b8 --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,9 @@ +import { ImageURISource } from "react-native"; + +/** + * type guard to check if a value is an ImageURISource + * @argument value the value to check, can be anything + * @returns boolean + */ +export const isImageUri = (value: unknown): value is ImageURISource => + typeof value === "object" && value !== null && "uri" in value;