diff --git a/ts/components/ui/FooterActions.tsx b/ts/components/ui/FooterActions.tsx index ac5f907bded..876a4454998 100644 --- a/ts/components/ui/FooterActions.tsx +++ b/ts/components/ui/FooterActions.tsx @@ -13,13 +13,7 @@ import { useIOTheme } from "@pagopa/io-app-design-system"; import * as React from "react"; -import { - ComponentProps, - Fragment, - PropsWithChildren, - useMemo, - useState -} from "react"; +import { ComponentProps, Fragment, PropsWithChildren, useState } from "react"; import { ColorValue, LayoutChangeEvent, @@ -143,10 +137,10 @@ export const FooterActions = ({ const theme = useIOTheme(); const { isExperimental } = useIOExperimentalDesign(); - const type = actions?.type; - const primaryAction = actions?.primary; - const secondaryAction = actions?.secondary; - const tertiaryAction = actions?.tertiary; + const { bottomMargin, extraBottomMargin } = useBottomMargins( + actions, + excludeSafeAreaMargins + ); /* Total height of actions */ const [actionBlockHeight, setActionBlockHeight] = @@ -158,41 +152,15 @@ export const FooterActions = ({ const TRANSPARENT_BG_COLOR: ColorValue = "transparent"; const BUTTONSOLID_HEIGHT = isExperimental ? buttonSolidHeight : 40; - const insets = useSafeAreaInsets(); - const needSafeAreaMargin = useMemo(() => insets.bottom !== 0, [insets]); - const safeAreaMargin = useMemo(() => insets.bottom, [insets]); - - /* Check if the iPhone bottom handle is present. - If not, or if you don't need safe area insets, - add a default margin to prevent the button - from sticking to the bottom. */ - const bottomMargin: number = useMemo( - () => - !needSafeAreaMargin || excludeSafeAreaMargins - ? IOVisualCostants.appMarginDefault - : safeAreaMargin, - [needSafeAreaMargin, excludeSafeAreaMargins, safeAreaMargin] - ); - - /* When the secondary action is visible, add extra margin - to avoid little space from iPhone bottom handle */ - const extraBottomMargin: number = useMemo( - () => (secondaryAction && needSafeAreaMargin ? extraSafeAreaMargin : 0), - [needSafeAreaMargin, secondaryAction] - ); - /* Safe background block. Cover everything until it reaches the half of the primary action button. It avoids glitchy behavior underneath. */ - const safeBackgroundBlockHeight: number = useMemo( - () => bottomMargin + actionBlockHeight - BUTTONSOLID_HEIGHT / 2, - [BUTTONSOLID_HEIGHT, actionBlockHeight, bottomMargin] - ); + const safeBackgroundBlockHeight = + bottomMargin + actionBlockHeight - BUTTONSOLID_HEIGHT / 2; const getActionBlockMeasurements = (event: LayoutChangeEvent) => { const { height } = event.nativeEvent.layout; setActionBlockHeight(height); - /* Height of the safe bottom area, applied to the ScrollView: Actions + Content end margin */ const safeBottomAreaHeight = bottomMargin + height + contentEndMargin; @@ -247,48 +215,76 @@ export const FooterActions = ({ {`Height: ${actionBlockHeight}`} )} - {primaryAction && } + {renderActions(actions, extraBottomMargin)} + + + ); +}; + +const useBottomMargins = ( + actions: FooterActions | undefined, + excludeSafeAreaMargins: boolean +) => { + const insets = useSafeAreaInsets(); + const needSafeAreaMargin = insets.bottom !== 0; - {type === "TwoButtons" && ( - - - {secondaryAction && ( - )} - /> - )} - - )} + /* Check if the iPhone bottom handle is present. + If not, or if you don't need safe area insets, + add a default margin to prevent the button + from sticking to the bottom. */ + const bottomMargin = + !needSafeAreaMargin || excludeSafeAreaMargins + ? IOVisualCostants.appMarginDefault + : insets.bottom; - {type === "ThreeButtons" && ( - - {secondaryAction && ( - - - - - )} + /* When the secondary action is visible, add extra margin + to avoid little space from iPhone bottom handle */ + const extraBottomMargin = + actions?.secondary && needSafeAreaMargin ? extraSafeAreaMargin : 0; - {tertiaryAction && ( - - - - - )} - - )} - - + return { bottomMargin, extraBottomMargin }; +}; + +const renderActions = ( + actions: FooterActions | undefined, + extraBottomMargin: number +) => { + if (!actions) { + return null; + } + const { + type, + primary: primaryAction, + secondary: secondaryAction, + tertiary: tertiaryAction + } = actions; + return ( + + {primaryAction && } + {type === "TwoButtons" && secondaryAction && ( + + + + + )} + {type === "ThreeButtons" && ( + <> + {secondaryAction && ( + <> + + + + )} + {tertiaryAction && ( + + + + + )} + + )} + ); }; diff --git a/ts/components/ui/IOScrollView.tsx b/ts/components/ui/IOScrollView.tsx index 245d9024fe1..0a3922e169f 100644 --- a/ts/components/ui/IOScrollView.tsx +++ b/ts/components/ui/IOScrollView.tsx @@ -1,3 +1,4 @@ +/* eslint-disable functional/immutable-data */ import { ButtonLink, ButtonOutline, @@ -17,7 +18,6 @@ import { Fragment, PropsWithChildren, useLayoutEffect, - useMemo, useState } from "react"; import { @@ -128,11 +128,6 @@ export const IOScrollView = ({ }: IOScrollView) => { const theme = useIOTheme(); - const type = actions?.type; - const primaryAction = actions?.primary; - const secondaryAction = actions?.secondary; - const tertiaryAction = actions?.tertiary; - /* Navigation */ const navigation = useNavigation(); @@ -151,20 +146,16 @@ export const IOScrollView = ({ }; const insets = useSafeAreaInsets(); - const needSafeAreaMargin = useMemo(() => insets.bottom !== 0, [insets]); - const safeAreaMargin = useMemo(() => insets.bottom, [insets]); + const needSafeAreaMargin = insets.bottom !== 0; /* Check if the iPhone bottom handle is present. If not, or if you don't need safe area insets, add a default margin to prevent the button from sticking to the bottom. */ - const bottomMargin: number = useMemo( - () => - !needSafeAreaMargin || excludeSafeAreaMargins - ? IOVisualCostants.appMarginDefault - : safeAreaMargin, - [needSafeAreaMargin, excludeSafeAreaMargins, safeAreaMargin] - ); + const bottomMargin = + !needSafeAreaMargin || excludeSafeAreaMargins + ? IOVisualCostants.appMarginDefault + : insets.bottom; /* GENERATE EASING GRADIENT Background color should be app main background @@ -182,30 +173,21 @@ export const IOScrollView = ({ /* When the secondary action is visible, add extra margin to avoid little space from iPhone bottom handle */ - const extraBottomMargin: number = useMemo( - () => (secondaryAction && needSafeAreaMargin ? extraSafeAreaMargin : 0), - [needSafeAreaMargin, secondaryAction] - ); + const extraBottomMargin = + actions?.secondary && needSafeAreaMargin ? extraSafeAreaMargin : 0; /* Safe background block. Cover at least 85% of the space to avoid glitchy elements underneath */ - const safeBackgroundBlockHeight: number = useMemo( - () => (bottomMargin + actionBlockHeight) * 0.85, - [actionBlockHeight, bottomMargin] - ); + const safeBackgroundBlockHeight = (bottomMargin + actionBlockHeight) * 0.85; /* Total height of "Actions + Gradient" area */ - const gradientAreaHeight: number = useMemo( - () => bottomMargin + actionBlockHeight + gradientSafeAreaHeight, - [actionBlockHeight, bottomMargin] - ); + const gradientAreaHeight = + bottomMargin + actionBlockHeight + gradientSafeAreaHeight; /* Height of the safe bottom area, applied to the ScrollView: Actions + Content end margin */ - const safeBottomAreaHeight: number = useMemo( - () => bottomMargin + actionBlockHeight + contentEndMargin, - [actionBlockHeight, bottomMargin] - ); + const safeBottomAreaHeight = + bottomMargin + actionBlockHeight + contentEndMargin; const handleScroll = useAnimatedScrollHandler( ({ contentOffset, layoutMeasurement, contentSize }) => { @@ -213,9 +195,7 @@ export const IOScrollView = ({ const maxScrollHeight = contentSize.height - layoutMeasurement.height; const scrollPercentage = scrollPosition / maxScrollHeight; - // eslint-disable-next-line functional/immutable-data scrollPositionAbsolute.value = scrollPosition; - // eslint-disable-next-line functional/immutable-data scrollPositionPercentage.value = scrollPercentage; } ); @@ -232,15 +212,12 @@ export const IOScrollView = ({ /* Set custom header with `react-navigation` library using `useLayoutEffect` hook */ - const scrollValues: IOSCrollViewHeaderScrollValues = useMemo( - () => ({ + useLayoutEffect(() => { + const scrollValues: IOSCrollViewHeaderScrollValues = { contentOffsetY: scrollPositionAbsolute, triggerOffset: snapOffset || 0 - }), - [scrollPositionAbsolute, snapOffset] - ); + }; - useLayoutEffect(() => { if (headerConfig) { navigation.setOptions({ header: () => ( @@ -249,7 +226,7 @@ export const IOScrollView = ({ headerTransparent: headerConfig.transparent }); } - }, [headerConfig, navigation, scrollValues]); + }, [headerConfig, navigation, scrollPositionAbsolute, snapOffset]); return ( @@ -314,8 +291,8 @@ export const IOScrollView = ({ {/* Safe background block. It's added because when you swipe up - quickly, the content below is visible for about 100ms. Without this - block, the content appears glitchy. */} + quickly, the content below is visible for about 100ms. Without this + block, the content appears glitchy. */} - - {primaryAction && } + {renderActionButtons(actions, extraBottomMargin)} + + + )} + + ); +}; - {type === "TwoButtons" && ( - - - {secondaryAction && ( - )} - /> - )} - - )} +const renderActionButtons = ( + actions: IOScrollViewActions, + extraBottomMargin: number +) => { + const { + type, + primary: primaryAction, + secondary: secondaryAction, + tertiary: tertiaryAction + } = actions; - {type === "ThreeButtons" && ( - - {secondaryAction && ( - - - - - )} + return ( + <> + {primaryAction && } - {tertiaryAction && ( - - - - - )} - - )} - + {type === "TwoButtons" && ( + + + )} + /> )} - + + {type === "ThreeButtons" && ( + + + + + + + + + + )} + ); }; diff --git a/ts/features/design-system/DesignSystem.tsx b/ts/features/design-system/DesignSystem.tsx index 5f42f5a9132..478def23625 100644 --- a/ts/features/design-system/DesignSystem.tsx +++ b/ts/features/design-system/DesignSystem.tsx @@ -1,16 +1,17 @@ -import { SectionList, StatusBar, View, useColorScheme } from "react-native"; -import * as React from "react"; import { - useIOTheme, Divider, - VSpacer, + IOVisualCostants, ListItemNav, - IOVisualCostants + VSpacer, + useIOTheme } from "@pagopa/io-app-design-system"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { useIONavigation } from "../../navigation/params/AppParamsList"; +import * as React from "react"; +import { SectionList, StatusBar, View, useColorScheme } from "react-native"; import { H1 } from "../../components/core/typography/H1"; import { LabelSmall } from "../../components/core/typography/LabelSmall"; +import { IOStyles } from "../../components/core/variables/IOStyles"; +import { useScreenEndMargin } from "../../hooks/useScreenEndMargin"; +import { useIONavigation } from "../../navigation/params/AppParamsList"; import DESIGN_SYSTEM_ROUTES from "./navigation/routes"; type SingleSectionProps = { @@ -40,7 +41,13 @@ const DATA_ROUTES_LEGACY: RoutesProps = Object.values( DESIGN_SYSTEM_ROUTES.LEGACY ); -const DESIGN_SYSTEM_SECTION_DATA = [ +type SectionDataProps = { + title: string; + description?: string; + data: RoutesProps; +}; + +const DESIGN_SYSTEM_SECTION_DATA: Array = [ { title: "Foundation", data: DATA_ROUTES_FOUNDATION @@ -74,6 +81,8 @@ export const DesignSystem = () => { const colorScheme = useColorScheme(); const navigation = useIONavigation(); + const { screenEndMargin } = useScreenEndMargin(); + const renderDSNavItem = ({ item: { title, route } }: { @@ -101,7 +110,13 @@ export const DesignSystem = () => { ); - const renderDSSectionFooter = () => ; + const renderDSSectionFooter = ({ section }: { section: SectionDataProps }) => + /* We exclude the last section because + we already apply the `screenEndMargin` */ + DESIGN_SYSTEM_SECTION_DATA.indexOf(section) !== + DESIGN_SYSTEM_SECTION_DATA.length - 1 ? ( + + ) : null; return ( <> @@ -115,7 +130,8 @@ export const DesignSystem = () => { contentContainerStyle={[ IOStyles.horizontalContentPadding, { - paddingTop: IOVisualCostants.appMarginDefault + paddingTop: IOVisualCostants.appMarginDefault, + paddingBottom: screenEndMargin } ]} renderSectionHeader={renderDSSection} diff --git a/ts/features/design-system/components/DesignSystemScreen.tsx b/ts/features/design-system/components/DesignSystemScreen.tsx index fe02f4e5038..c76b3966a51 100644 --- a/ts/features/design-system/components/DesignSystemScreen.tsx +++ b/ts/features/design-system/components/DesignSystemScreen.tsx @@ -1,11 +1,11 @@ -import * as React from "react"; -import { ScrollView, StatusBar, View, useColorScheme } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ContentWrapper, IOVisualCostants, useIOTheme } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { ScrollView, StatusBar, View, useColorScheme } from "react-native"; +import { useScreenEndMargin } from "../../../hooks/useScreenEndMargin"; type Props = { title: string; @@ -15,9 +15,10 @@ type Props = { export const DesignSystemScreen = ({ children, noMargin = false }: Props) => { const colorScheme = useColorScheme(); - const insets = useSafeAreaInsets(); const theme = useIOTheme(); + const { screenEndMargin } = useScreenEndMargin(); + return ( <> { {noMargin ? ( diff --git a/ts/features/design-system/core/DSScreenEndMargin.tsx b/ts/features/design-system/core/DSScreenEndMargin.tsx new file mode 100644 index 00000000000..a136d1835f7 --- /dev/null +++ b/ts/features/design-system/core/DSScreenEndMargin.tsx @@ -0,0 +1,21 @@ +import { Body, IOVisualCostants } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { ScrollView } from "react-native"; +import { useScreenEndMargin } from "../../../hooks/useScreenEndMargin"; + +export const DSScreenEndMargin = () => { + const { screenEndMargin } = useScreenEndMargin(); + + return ( + + {[...Array(50)].map((_el, i) => ( + Repeated text + ))} + + ); +}; diff --git a/ts/features/design-system/navigation/navigator.tsx b/ts/features/design-system/navigation/navigator.tsx index dd1ff793e91..024ef194656 100644 --- a/ts/features/design-system/navigation/navigator.tsx +++ b/ts/features/design-system/navigation/navigator.tsx @@ -64,6 +64,7 @@ import { DSWizardScreen } from "../core/DSWizardScreen"; import DSListItemScreen from "../core/DSListItemScreen"; import { DSFooterActions } from "../core/DSFooterActions"; import { DSFooterActionsNotFixed } from "../core/DSFooterActionsNotFixed"; +import { DSScreenEndMargin } from "../core/DSScreenEndMargin"; import { DesignSystemParamsList } from "./params"; import DESIGN_SYSTEM_ROUTES from "./routes"; @@ -436,6 +437,14 @@ export const DesignSystemNavigator = () => { options={{ headerShown: false }} /> + + { const route = useRoute>(); + const navigation = useNavigation(); + const { screenEndMargin } = useScreenEndMargin(); + // Navigation prop const { faqCategories, @@ -223,45 +232,52 @@ const ZendeskSupportHelpCenter = () => { addTicketCustomField(zendeskFciId, signatureRequestId ?? ""); } + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: true, + header: () => ( + + ) + }); + }); + return ( - } - customRightIcon={{ - iconName: "closeLarge", - onPress: workUnitCancel, - accessibilityLabel: I18n.t("global.accessibility.contextualHelp.close") - }} - headerTitle={I18n.t("support.helpCenter.header")} + - - - + + - {showRequestSupportContacts && ( - <> - - - - )} - - - - + {showRequestSupportContacts && ( + <> + + + + )} + + ); }; diff --git a/ts/hooks/useScreenEndMargin.tsx b/ts/hooks/useScreenEndMargin.tsx new file mode 100644 index 00000000000..263bbe1efbd --- /dev/null +++ b/ts/hooks/useScreenEndMargin.tsx @@ -0,0 +1,51 @@ +import { IOSpacingScale, IOVisualCostants } from "@pagopa/io-app-design-system"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +type EndScreenSpacingValues = { + screenEndSafeArea: number; + screenEndMargin: number; +}; + +/** + * A custom React Hook that returns two spacing values that must be applied at + * the end of a ScrollView to prevent the content from being cut off. + * Depending on what you need, you can set one of these two values as `paddingBottom` + * using ScrollView's `contentContainerStyle` prop: + * - `screenEndSafeArea` + * The amount of safe area without additional margins. For devices that don't have safe area + * boundaries (e.g. iPhone with home button) it returns a fallback value that prevents content + * from sticking to the bottom. + * - `screenEndMargin` + * The total amount of space to add at the end of the ScrollView. It's a sum of the + * `screenSafeArea' value and the default `contentEndMargin' that should be applied + * at the end of each app screen. + */ +export const useScreenEndMargin = (): EndScreenSpacingValues => { + const insets = useSafeAreaInsets(); + + const needSafeAreaMargin = insets.bottom !== 0; + + /* We use this fallback value to ensure that the spacing + is is consistent across all axes. */ + const fallbackSafeAreaMargin = IOVisualCostants.appMarginDefault; + + /* End content margin. If the devices don't have safe area + boundaries, we calculate the difference to get the same spacing + value as for devices that do have safe area boundaries. */ + const contentEndMargin: IOSpacingScale = 32; + const computedContentEndMargin = needSafeAreaMargin + ? contentEndMargin + : contentEndMargin - fallbackSafeAreaMargin; + + /* Check if the iPhone bottom handle is present. + If not add a default margin to prevent the button + from sticking to the bottom. */ + const screenEndSafeArea = !needSafeAreaMargin + ? fallbackSafeAreaMargin + : insets.bottom; + + return { + screenEndSafeArea, + screenEndMargin: screenEndSafeArea + computedContentEndMargin + }; +};