From de03c6f6bc8c6969f16f5ead8ffdee07cc9f8147 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Tue, 9 Apr 2024 15:18:40 +0200 Subject: [PATCH 01/15] Refactor code, add dark mode support --- example/src/pages/GradientScroll.tsx | 90 +++++----- src/components/layout/GradientScrollView.tsx | 177 ++++++++++++++++--- 2 files changed, 200 insertions(+), 67 deletions(-) diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx index 32f3acb4..4ffc8693 100644 --- a/example/src/pages/GradientScroll.tsx +++ b/example/src/pages/GradientScroll.tsx @@ -4,50 +4,60 @@ import { GradientScrollView, H2, IOColors, - VSpacer + VSpacer, + useIOTheme } from "@pagopa/io-app-design-system"; import * as React from "react"; import { Alert, View } from "react-native"; -export const GradientScroll = () => ( - - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") +export const GradientScroll = () => { + const theme = useIOTheme(); + + return ( + -

Start

- {[...Array(50)].map((_el, i) => ( - Repeated text - ))} - - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + }, + secondary: { + label: "Secondary", + onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + } }} - /> - - Alert.alert("Test button")} - /> - {[...Array(2)].map((_el, i) => ( - Repeated text - ))} -

End

-
-
-); + > +

Start

+ {[...Array(50)].map((_el, i) => ( + Repeated text + ))} + + + + Alert.alert("Test button")} + /> + {[...Array(2)].map((_el, i) => ( + Repeated text + ))} +

End

+ +
+ ); +}; diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index 900fcc27..7570ec21 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -1,5 +1,8 @@ import * as React from "react"; -import { useMemo } from "react"; +import { ComponentProps, PropsWithChildren, useMemo } from "react"; +import { ColorValue, StyleSheet, View } from "react-native"; +import { easeGradient } from "react-native-easing-gradient"; +import LinearGradient from "react-native-linear-gradient"; import Animated, { Easing, useAnimatedScrollHandler, @@ -9,23 +12,42 @@ import Animated, { } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { + IOColors, IOSpacer, IOSpacingScale, IOVisualCostants, - buttonSolidHeight + buttonSolidHeight, + hexToRgba, + useIOTheme } from "../../core"; import { WithTestID } from "../../utils/types"; -import GradientBottomActions from "./GradientBottomActions"; - -export type GradientScrollView = WithTestID<{ - children: React.ReactNode; - excludeSafeAreaMargins?: boolean; - debugMode?: boolean; - // Accepted components: ButtonSolid, ButtonLink - // Don't use any components other than this, please. - primaryActionProps: GradientBottomActions["primaryActionProps"]; - secondaryActionProps?: GradientBottomActions["secondaryActionProps"]; -}>; +import { ButtonLink, ButtonOutline, ButtonSolid } from "../buttons"; +import { VSpacer } from "../spacer"; + +type GradientScrollActions = + | { + primary: Omit, "fullWidth">; + secondary?: never; + tertiary?: never; + } + | { + primary: Omit, "fullWidth">; + secondary: ComponentProps; + tertiary?: never; + } + | { + primary: Omit, "fullWidth">; + secondary: Omit, "fullWidth">; + tertiary: ComponentProps; + }; + +type GradientScrollView = WithTestID< + PropsWithChildren<{ + excludeSafeAreaMargins?: boolean; + debugMode?: boolean; + actionsProps: GradientScrollActions; + }> +>; // Extended gradient area above the actions export const gradientSafeArea: IOSpacingScale = 96; @@ -38,17 +60,40 @@ const secondaryActionEstHeight: number = 20; // Extra bottom margin for iPhone bottom handle const extraSafeAreaMargin: IOSpacingScale = 8; +const styles = StyleSheet.create({ + buttonContainer: { + paddingHorizontal: IOVisualCostants.appMarginDefault, + width: "100%", + flex: 1, + flexShrink: 0, + justifyContent: "flex-end" + }, + gradientContainer: { + ...StyleSheet.absoluteFillObject + }, + safeBackgroundBlock: { + position: "absolute", + bottom: 0, + left: 0, + right: 0 + } +}); + export const GradientScrollView = ({ children, - primaryActionProps: primaryActionProps, - secondaryActionProps: secondaryActionProps, + actionsProps: { primary: primaryAction, secondary: secondaryAction }, // Don't include safe area insets excludeSafeAreaMargins = false, debugMode = false, testID }: GradientScrollView) => { const gradientOpacity = useSharedValue(1); + + const theme = useIOTheme(); + const insets = useSafeAreaInsets(); + const isSafeAreaMarginNeeded = 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, @@ -56,30 +101,42 @@ export const GradientScrollView = ({ from sticking to the bottom. */ const bottomMargin: number = useMemo( () => - insets.bottom === 0 || excludeSafeAreaMargins + isSafeAreaMarginNeeded || excludeSafeAreaMargins ? IOVisualCostants.appMarginDefault - : insets.bottom, - [insets, excludeSafeAreaMargins] + : safeAreaMargin, + [isSafeAreaMarginNeeded, excludeSafeAreaMargins, safeAreaMargin] ); + // GENERATE EASING GRADIENT + // Background color should be app main background (both light and dark themes) + const HEADER_BG_COLOR: ColorValue = IOColors[theme["appBackground-primary"]]; + + const { colors, locations } = easeGradient({ + colorStops: { + 0: { color: hexToRgba(HEADER_BG_COLOR, 0) }, + 1: { color: HEADER_BG_COLOR } + }, + easing: Easing.ease, + extraColorStopsPerTransition: 20 + }); + /* When the secondary action is visible, add extra margin to avoid little space from iPhone bottom handle */ const extraBottomMargin: number = useMemo( - () => - secondaryActionProps && insets.bottom !== 0 ? extraSafeAreaMargin : 0, - [insets.bottom, secondaryActionProps] + () => (secondaryAction && isSafeAreaMarginNeeded ? extraSafeAreaMargin : 0), + [isSafeAreaMarginNeeded, secondaryAction] ); /* Total height of actions */ const actionsArea: number = useMemo( () => - primaryActionProps && secondaryActionProps + primaryAction && secondaryAction ? (buttonSolidHeight as number) + spaceBetweenActions + secondaryActionEstHeight + extraBottomMargin : buttonSolidHeight, - [extraBottomMargin, primaryActionProps, secondaryActionProps] + [extraBottomMargin, primaryAction, secondaryAction] ); /* Total height of "Actions + Gradient" area */ @@ -104,13 +161,13 @@ to avoid little space from iPhone bottom handle */ const safeBackgroundHeight = useMemo( () => - secondaryActionProps + secondaryAction ? spaceBetweenActions + secondaryActionEstHeight + extraBottomMargin + bottomMargin : bottomMargin, - [bottomMargin, extraBottomMargin, secondaryActionProps] + [bottomMargin, extraBottomMargin, secondaryAction] ); const handleScroll = useAnimatedScrollHandler( @@ -148,7 +205,73 @@ to avoid little space from iPhone bottom handle */ > {children} - + + + {/* 100% opacity bg color fills at least 45% of the area */} + + + + + + + + {primaryAction && ( + + )} + + {secondaryAction && ( + + + {} + + )} + + + {/* + /> */} ); }; From cbb108e4d5c9e472440194b40298413db11c33e2 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Tue, 9 Apr 2024 16:12:58 +0200 Subject: [PATCH 02/15] Remove unnecessary hardcoded layout calculations --- src/components/layout/GradientScrollView.tsx | 119 ++++++++++--------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index 7570ec21..d5a6f0b1 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -1,6 +1,12 @@ import * as React from "react"; -import { ComponentProps, PropsWithChildren, useMemo } from "react"; -import { ColorValue, StyleSheet, View } from "react-native"; +import { ComponentProps, PropsWithChildren, useMemo, useState } from "react"; +import { + ColorValue, + LayoutChangeEvent, + LayoutRectangle, + StyleSheet, + View +} from "react-native"; import { easeGradient } from "react-native-easing-gradient"; import LinearGradient from "react-native-linear-gradient"; import Animated, { @@ -16,7 +22,6 @@ import { IOSpacer, IOSpacingScale, IOVisualCostants, - buttonSolidHeight, hexToRgba, useIOTheme } from "../../core"; @@ -43,30 +48,32 @@ type GradientScrollActions = type GradientScrollView = WithTestID< PropsWithChildren<{ + actionsProps: GradientScrollActions; excludeSafeAreaMargins?: boolean; debugMode?: boolean; - actionsProps: GradientScrollActions; }> >; // Extended gradient area above the actions -export const gradientSafeArea: IOSpacingScale = 96; +export const gradientSafeAreaHeight: IOSpacingScale = 96; // End content margin before the actions const contentEndMargin: IOSpacingScale = 32; // Margin between primary action and secondary one const spaceBetweenActions: IOSpacer = 24; -// Estimated height of the secondary action -const secondaryActionEstHeight: number = 20; // Extra bottom margin for iPhone bottom handle const extraSafeAreaMargin: IOSpacingScale = 8; const styles = StyleSheet.create({ + gradientBottomActions: { + width: "100%", + position: "absolute", + bottom: 0, + justifyContent: "flex-end" + }, buttonContainer: { paddingHorizontal: IOVisualCostants.appMarginDefault, width: "100%", - flex: 1, - flexShrink: 0, - justifyContent: "flex-end" + flexShrink: 0 }, gradientContainer: { ...StyleSheet.absoluteFillObject @@ -81,24 +88,37 @@ const styles = StyleSheet.create({ export const GradientScrollView = ({ children, - actionsProps: { primary: primaryAction, secondary: secondaryAction }, + actionsProps: { + primary: primaryAction, + secondary: secondaryAction, + tertiary: tertiaryAction + }, // Don't include safe area insets excludeSafeAreaMargins = false, debugMode = false, testID }: GradientScrollView) => { + const theme = useIOTheme(); + + /* Shared Values for `reanimated` */ const gradientOpacity = useSharedValue(1); - const theme = useIOTheme(); + /* Total height of actions */ + const [actionBlockHeight, setActionBlockHeight] = + useState(0); + + const getActionBlockHeight = (event: LayoutChangeEvent) => { + setActionBlockHeight(event.nativeEvent.layout.height); + }; const insets = useSafeAreaInsets(); const isSafeAreaMarginNeeded = 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. */ + 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( () => isSafeAreaMarginNeeded || excludeSafeAreaMargins @@ -107,8 +127,9 @@ export const GradientScrollView = ({ [isSafeAreaMarginNeeded, excludeSafeAreaMargins, safeAreaMargin] ); - // GENERATE EASING GRADIENT - // Background color should be app main background (both light and dark themes) + /* GENERATE EASING GRADIENT + Background color should be app main background + (both light and dark themes) */ const HEADER_BG_COLOR: ColorValue = IOColors[theme["appBackground-primary"]]; const { colors, locations } = easeGradient({ @@ -121,53 +142,33 @@ export const GradientScrollView = ({ }); /* When the secondary action is visible, add extra margin -to avoid little space from iPhone bottom handle */ + to avoid little space from iPhone bottom handle */ const extraBottomMargin: number = useMemo( () => (secondaryAction && isSafeAreaMarginNeeded ? extraSafeAreaMargin : 0), [isSafeAreaMarginNeeded, secondaryAction] ); - /* Total height of actions */ - const actionsArea: number = useMemo( - () => - primaryAction && secondaryAction - ? (buttonSolidHeight as number) + - spaceBetweenActions + - secondaryActionEstHeight + - extraBottomMargin - : buttonSolidHeight, - [extraBottomMargin, primaryAction, secondaryAction] - ); - /* Total height of "Actions + Gradient" area */ const gradientAreaHeight: number = useMemo( - () => bottomMargin + actionsArea + gradientSafeArea, - [actionsArea, bottomMargin] + () => bottomMargin + actionBlockHeight + gradientSafeAreaHeight, + [actionBlockHeight, bottomMargin] ); /* Height of the safe bottom area, applied to the ScrollView: - Actions + Content end margin */ + Actions + Content end margin */ const safeBottomAreaHeight: number = useMemo( - () => bottomMargin + actionsArea + contentEndMargin, - [actionsArea, bottomMargin] + () => bottomMargin + actionBlockHeight + contentEndMargin, + [actionBlockHeight, bottomMargin] ); - { - /* 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. */ - } + /* 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. */ const safeBackgroundHeight = useMemo( - () => - secondaryAction - ? spaceBetweenActions + - secondaryActionEstHeight + - extraBottomMargin + - bottomMargin - : bottomMargin, - [bottomMargin, extraBottomMargin, secondaryAction] + () => actionBlockHeight * 0.5 + bottomMargin, + [actionBlockHeight, bottomMargin] ); const handleScroll = useAnimatedScrollHandler( @@ -206,13 +207,13 @@ to avoid little space from iPhone bottom handle */ {children} @@ -253,7 +254,11 @@ to avoid little space from iPhone bottom handle */ } ]} /> - + {primaryAction && ( )} From 54b1000f1bf211d18549722d0e6fc60830a3968e Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Tue, 9 Apr 2024 16:21:00 +0200 Subject: [PATCH 03/15] Improve `DebugMode` --- src/components/layout/GradientScrollView.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index d5a6f0b1..5b2e1937 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -221,14 +221,21 @@ export const GradientScrollView = ({ style={[ styles.gradientContainer, debugMode && { - borderTopColor: IOColors["error-500"], - borderTopWidth: 1, - backgroundColor: hexToRgba(IOColors["error-500"], 0.5) + backgroundColor: hexToRgba(IOColors["error-500"], 0.15) } ]} pointerEvents="none" > - + {/* 100% opacity bg color fills at least 45% of the area */} Date: Tue, 9 Apr 2024 17:10:08 +0200 Subject: [PATCH 04/15] Remove `safeBackgroundBlock` because not necessary anymore --- example/src/pages/GradientScroll.tsx | 8 ++--- src/components/layout/GradientScrollView.tsx | 37 +++++--------------- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx index 4ffc8693..efbc7b99 100644 --- a/example/src/pages/GradientScroll.tsx +++ b/example/src/pages/GradientScroll.tsx @@ -26,11 +26,11 @@ export const GradientScroll = () => { primary: { label: "Primary action", onPress: () => Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - }, - secondary: { - label: "Secondary", - onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") } + // secondary: { + // label: "Secondary", + // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // } }} >

Start

diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index 5b2e1937..5dab7a31 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -59,7 +59,7 @@ export const gradientSafeAreaHeight: IOSpacingScale = 96; // End content margin before the actions const contentEndMargin: IOSpacingScale = 32; // Margin between primary action and secondary one -const spaceBetweenActions: IOSpacer = 24; +const spaceBetweenActions: IOSpacer = 16; // Extra bottom margin for iPhone bottom handle const extraSafeAreaMargin: IOSpacingScale = 8; @@ -77,12 +77,6 @@ const styles = StyleSheet.create({ }, gradientContainer: { ...StyleSheet.absoluteFillObject - }, - safeBackgroundBlock: { - position: "absolute", - bottom: 0, - left: 0, - right: 0 } }); @@ -161,16 +155,6 @@ export const GradientScrollView = ({ [actionBlockHeight, bottomMargin] ); - /* 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. */ - - const safeBackgroundHeight = useMemo( - () => actionBlockHeight * 0.5 + bottomMargin, - [actionBlockHeight, bottomMargin] - ); - const handleScroll = useAnimatedScrollHandler( ({ contentOffset, layoutMeasurement, contentSize }) => { /* We use Math.floor because decimals used on Android @@ -236,31 +220,26 @@ export const GradientScrollView = ({ } ]} > - {/* 100% opacity bg color fills at least 45% of the area */} + {/* 100% opacity bg color fills at least 40% of the area */}
+ + {/* 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. */}
- Date: Tue, 9 Apr 2024 18:02:14 +0200 Subject: [PATCH 05/15] Fix wrong safe area margin condition, manage three buttons --- example/src/pages/GradientScroll.tsx | 13 ++- src/components/layout/GradientScrollView.tsx | 87 +++++++++++++------- 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx index efbc7b99..2654d0a3 100644 --- a/example/src/pages/GradientScroll.tsx +++ b/example/src/pages/GradientScroll.tsx @@ -23,14 +23,19 @@ export const GradientScroll = () => { Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + }, + secondary: { + label: "Secondary", + onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + }, + tertiary: { + label: "Tertiary", + onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") } - // secondary: { - // label: "Secondary", - // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // } }} >

Start

diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index 5dab7a31..f129f823 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -31,16 +31,19 @@ import { VSpacer } from "../spacer"; type GradientScrollActions = | { + type: "SingleButton"; primary: Omit, "fullWidth">; secondary?: never; tertiary?: never; } | { + type: "TwoButtons"; primary: Omit, "fullWidth">; secondary: ComponentProps; tertiary?: never; } | { + type: "ThreeButtons"; primary: Omit, "fullWidth">; secondary: Omit, "fullWidth">; tertiary: ComponentProps; @@ -54,13 +57,16 @@ type GradientScrollView = WithTestID< }> >; -// Extended gradient area above the actions +/* Extended gradient area above the actions */ export const gradientSafeAreaHeight: IOSpacingScale = 96; -// End content margin before the actions +/* End content margin before the actions */ const contentEndMargin: IOSpacingScale = 32; -// Margin between primary action and secondary one -const spaceBetweenActions: IOSpacer = 16; -// Extra bottom margin for iPhone bottom handle +/* Margin between ButtonSolid and ButtonOutline */ +const spaceBetweenActions: IOSpacer = 8; +/* Margin between ButtonSolid and ButtonLink */ +const spaceBetweenActionAndLink: IOSpacer = 16; +/* Extra bottom margin for iPhone bottom handle because + ButtonLink doesn't have a fixed height */ const extraSafeAreaMargin: IOSpacingScale = 8; const styles = StyleSheet.create({ @@ -83,6 +89,7 @@ const styles = StyleSheet.create({ export const GradientScrollView = ({ children, actionsProps: { + type, primary: primaryAction, secondary: secondaryAction, tertiary: tertiaryAction @@ -106,7 +113,7 @@ export const GradientScrollView = ({ }; const insets = useSafeAreaInsets(); - const isSafeAreaMarginNeeded = useMemo(() => insets.bottom !== 0, [insets]); + const needSafeAreaMargin = useMemo(() => insets.bottom !== 0, [insets]); const safeAreaMargin = useMemo(() => insets.bottom, [insets]); /* Check if the iPhone bottom handle is present. @@ -115,10 +122,15 @@ export const GradientScrollView = ({ from sticking to the bottom. */ const bottomMargin: number = useMemo( () => - isSafeAreaMarginNeeded || excludeSafeAreaMargins + !needSafeAreaMargin || excludeSafeAreaMargins ? IOVisualCostants.appMarginDefault : safeAreaMargin, - [isSafeAreaMarginNeeded, excludeSafeAreaMargins, safeAreaMargin] + [needSafeAreaMargin, excludeSafeAreaMargins, safeAreaMargin] + ); + + // eslint-disable-next-line no-console + console.log( + `${needSafeAreaMargin} - Safe margin: ${safeAreaMargin} — Bottom margin: ${bottomMargin}` ); /* GENERATE EASING GRADIENT @@ -138,8 +150,15 @@ export const GradientScrollView = ({ /* When the secondary action is visible, add extra margin to avoid little space from iPhone bottom handle */ const extraBottomMargin: number = useMemo( - () => (secondaryAction && isSafeAreaMarginNeeded ? extraSafeAreaMargin : 0), - [isSafeAreaMarginNeeded, secondaryAction] + () => (secondaryAction && needSafeAreaMargin ? extraSafeAreaMargin : 0), + [needSafeAreaMargin, secondaryAction] + ); + + /* Safe background block. Learn more below where the + block is placed */ + const safeBackgroundBlockHeight: number = useMemo( + () => bottomMargin + actionBlockHeight, + [actionBlockHeight, bottomMargin] ); /* Total height of "Actions + Gradient" area */ @@ -220,7 +239,6 @@ export const GradientScrollView = ({ } ]} > - {/* 100% opacity bg color fills at least 40% of the area */} @@ -245,36 +263,43 @@ export const GradientScrollView = ({ onLayout={getActionBlockHeight} pointerEvents="box-none" > - {primaryAction && ( - - )} + {primaryAction && } - {secondaryAction && ( + {type === "TwoButtons" && secondaryAction && ( - - {} + + )} + + {type === "ThreeButtons" && ( + <> + {secondaryAction && ( + <> + + + + )} + {tertiaryAction && ( + + + + + )} + + )}
- {/* */} ); }; From 1639a84a81f4d299a69db57c308ff899fc2b3440 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Tue, 9 Apr 2024 18:19:32 +0200 Subject: [PATCH 06/15] Fix size of `safeBackgroundBlock` --- src/components/layout/GradientScrollView.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index f129f823..7113da5f 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -128,11 +128,6 @@ export const GradientScrollView = ({ [needSafeAreaMargin, excludeSafeAreaMargins, safeAreaMargin] ); - // eslint-disable-next-line no-console - console.log( - `${needSafeAreaMargin} - Safe margin: ${safeAreaMargin} — Bottom margin: ${bottomMargin}` - ); - /* GENERATE EASING GRADIENT Background color should be app main background (both light and dark themes) */ @@ -154,10 +149,10 @@ export const GradientScrollView = ({ [needSafeAreaMargin, secondaryAction] ); - /* Safe background block. Learn more below where the - block is placed */ + /* Safe background block. Cover at least 85% of the space + to avoid glitchy elements underneath */ const safeBackgroundBlockHeight: number = useMemo( - () => bottomMargin + actionBlockHeight, + () => (bottomMargin + actionBlockHeight) * 0.85, [actionBlockHeight, bottomMargin] ); @@ -240,7 +235,7 @@ export const GradientScrollView = ({ ]} > From c67d69931859ca90454bde9c0ac60d95a251c1dc Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Tue, 9 Apr 2024 18:24:32 +0200 Subject: [PATCH 07/15] Remove `GradientBottomActions` --- .../layout/GradientBottomActions.tsx | 140 ------------------ 1 file changed, 140 deletions(-) delete mode 100644 src/components/layout/GradientBottomActions.tsx diff --git a/src/components/layout/GradientBottomActions.tsx b/src/components/layout/GradientBottomActions.tsx deleted file mode 100644 index 04e75c61..00000000 --- a/src/components/layout/GradientBottomActions.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import * as React from "react"; -import { Easing, StyleProp, StyleSheet, View, ViewStyle } from "react-native"; -import { easeGradient } from "react-native-easing-gradient"; -import LinearGradient from "react-native-linear-gradient"; -import Animated from "react-native-reanimated"; -import { IOColors, IOSpacer, IOVisualCostants, hexToRgba } from "../../core"; -import { WithTestID } from "../../utils/types"; -import { - ButtonLink, - ButtonLinkProps, - ButtonSolid, - ButtonSolidProps -} from "../buttons"; -import { VSpacer } from "../spacer"; - -export type GradientBottomActions = WithTestID<{ - transitionAnimStyle: Animated.AnimateStyle>; - dimensions: GradientBottomActionsDimensions; - // Accepted components: ButtonSolid for the primaryAction, ButtonLink for the secondaryAction - primaryActionProps?: Omit; - secondaryActionProps?: ButtonLinkProps; - // Debug mode - debugMode?: boolean; -}>; - -type GradientBottomActionsDimensions = { - bottomMargin: number; - extraBottomMargin: number; - gradientAreaHeight: number; - spaceBetweenActions: IOSpacer; - safeBackgroundHeight: number; -}; - -// Background color should be app main background (both light and dark themes) -const HEADER_BG_COLOR: IOColors = "white"; - -const { colors, locations } = easeGradient({ - colorStops: { - 0: { color: hexToRgba(IOColors[HEADER_BG_COLOR], 0) }, - 1: { color: IOColors[HEADER_BG_COLOR] } - }, - easing: Easing.ease, - extraColorStopsPerTransition: 20 -}); - -const styles = StyleSheet.create({ - buttonContainer: { - paddingHorizontal: IOVisualCostants.appMarginDefault, - width: "100%", - flex: 1, - flexShrink: 0, - justifyContent: "flex-end" - }, - gradientContainer: { - ...StyleSheet.absoluteFillObject - }, - safeBackgroundBlock: { - position: "absolute", - bottom: 0, - left: 0, - right: 0, - backgroundColor: IOColors[HEADER_BG_COLOR] - } -}); - -export const GradientBottomActions = ({ - primaryActionProps: primaryAction, - secondaryActionProps: secondaryAction, - dimensions, - transitionAnimStyle, - debugMode, - testID -}: GradientBottomActions) => ( - - - {/* 100% opacity bg color fills at least 45% of the area */} - - - - - - - {primaryAction && ( - - )} - - {secondaryAction && ( - - - {} - - )} - - -); - -export default GradientBottomActions; From fbb766c7f534ff4c83b0a254feca580a7de66c56 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Wed, 10 Apr 2024 09:23:41 +0200 Subject: [PATCH 08/15] Remove export of `GradientBottomActions` --- src/components/layout/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index a6738840..4e312c6e 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -1,9 +1,8 @@ -export * from "./common"; +export * from "./BlockButtons"; +export * from "./FooterWithButtons"; +export * from "./ForceScrollDownView"; export * from "./GradientScrollView"; -export * from "./GradientBottomActions"; export * from "./HeaderFirstLevel"; export * from "./HeaderSecondLevel"; -export * from "./ForceScrollDownView"; -export * from "./FooterWithButtons"; -export * from "./BlockButtons"; export * from "./ModalBSHeader"; +export * from "./common"; From f27e3ae50dc70a36a0234ccc91e8a5883bd0515d Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Wed, 10 Apr 2024 10:15:09 +0200 Subject: [PATCH 09/15] Change opacity transition from discrete to continuous --- example/src/pages/GradientScroll.tsx | 10 +++--- src/components/layout/GradientScrollView.tsx | 36 +++++++++++--------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx index 2654d0a3..fd31ff06 100644 --- a/example/src/pages/GradientScroll.tsx +++ b/example/src/pages/GradientScroll.tsx @@ -23,7 +23,7 @@ export const GradientScroll = () => { Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") @@ -31,11 +31,11 @@ export const GradientScroll = () => { secondary: { label: "Secondary", onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - }, - tertiary: { - label: "Tertiary", - onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") } + // tertiary: { + // label: "Tertiary", + // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // } }} >

Start

diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index 7113da5f..31b114b9 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -11,10 +11,11 @@ import { easeGradient } from "react-native-easing-gradient"; import LinearGradient from "react-native-linear-gradient"; import Animated, { Easing, + Extrapolate, + interpolate, useAnimatedScrollHandler, useAnimatedStyle, - useSharedValue, - withTiming + useSharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { @@ -57,8 +58,11 @@ type GradientScrollView = WithTestID< }> >; +/* Percentage of scrolled content that triggers + the gradient opaciy transition */ +const gradientOpacityScrollTrigger = 0.85; /* Extended gradient area above the actions */ -export const gradientSafeAreaHeight: IOSpacingScale = 96; +const gradientSafeAreaHeight: IOSpacingScale = 96; /* End content margin before the actions */ const contentEndMargin: IOSpacingScale = 32; /* Margin between ButtonSolid and ButtonOutline */ @@ -102,7 +106,7 @@ export const GradientScrollView = ({ const theme = useIOTheme(); /* Shared Values for `reanimated` */ - const gradientOpacity = useSharedValue(1); + const scrollPositionPercentage = useSharedValue(0); /* Scroll position */ /* Total height of actions */ const [actionBlockHeight, setActionBlockHeight] = @@ -171,24 +175,22 @@ export const GradientScrollView = ({ const handleScroll = useAnimatedScrollHandler( ({ contentOffset, layoutMeasurement, contentSize }) => { - /* We use Math.floor because decimals used on Android - devices never change the `isEndReached` boolean value. - We have more consistent behavior across platforms - if we round these calculations ¯\_(ツ)_/¯ */ - const isEndReached = - Math.floor(layoutMeasurement.height + contentOffset.y) >= - Math.floor(contentSize.height); + const scrollPosition = contentOffset.y; + const maxScrollHeight = contentSize.height - layoutMeasurement.height; + const scrollPercentage = scrollPosition / maxScrollHeight; // eslint-disable-next-line functional/immutable-data - gradientOpacity.value = isEndReached ? 0 : 1; + scrollPositionPercentage.value = scrollPercentage; } ); const opacityTransition = useAnimatedStyle(() => ({ - opacity: withTiming(gradientOpacity.value, { - duration: 200, - easing: Easing.ease - }) + opacity: interpolate( + scrollPositionPercentage.value, + [0, gradientOpacityScrollTrigger, 1], + [1, 1, 0], + Extrapolate.CLAMP + ) })); return ( @@ -196,7 +198,7 @@ export const GradientScrollView = ({ Date: Wed, 10 Apr 2024 10:29:47 +0200 Subject: [PATCH 10/15] Update example screen with dark mode compatible headers --- example/src/pages/GradientScroll.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx index fd31ff06..02eb8f5b 100644 --- a/example/src/pages/GradientScroll.tsx +++ b/example/src/pages/GradientScroll.tsx @@ -21,7 +21,7 @@ export const GradientScroll = () => { }} > { // } }} > -

Start

+

Start

{[...Array(50)].map((_el, i) => ( Repeated text ))} @@ -61,7 +61,7 @@ export const GradientScroll = () => { {[...Array(2)].map((_el, i) => ( Repeated text ))} -

End

+

End

); From 1faf695dc38a5b1fd89ed9d57c92fb23783b3a2b Mon Sep 17 00:00:00 2001 From: Cristiano Tofani Date: Wed, 10 Apr 2024 15:42:52 +0200 Subject: [PATCH 11/15] update typescript version for compilation error --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7c298423..6ab6269b 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "storybook": "^7.4.0", "storybook-react-context": "^0.6.0", "svgo": "^3.0.2", - "typescript": "^4.9.5" + "typescript": "^5.4.4" }, "resolutions": { "@types/react": "17.0.43" diff --git a/yarn.lock b/yarn.lock index 4626b815..a6495c4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14655,10 +14655,10 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.4.tgz#eb2471e7b0a5f1377523700a21669dce30c2d952" + integrity sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw== ua-parser-js@^1.0.35: version "1.0.35" From 8d550e438ff6cc542bedba94fa365bafda2915a7 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Thu, 11 Apr 2024 09:46:46 +0200 Subject: [PATCH 12/15] Hide gradient if `actionProps` is not defined --- example/src/pages/GradientScroll.tsx | 32 ++-- src/components/layout/GradientScrollView.tsx | 179 ++++++++++--------- 2 files changed, 111 insertions(+), 100 deletions(-) diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx index 02eb8f5b..d7462d91 100644 --- a/example/src/pages/GradientScroll.tsx +++ b/example/src/pages/GradientScroll.tsx @@ -21,22 +21,22 @@ export const GradientScroll = () => { }} > Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - }, - secondary: { - label: "Secondary", - onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - } - // tertiary: { - // label: "Tertiary", - // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // } - }} + // debugMode + // actionsProps={{ + // type: "ThreeButtons", + // primary: { + // label: "Primary action", + // onPress: () => Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // }, + // secondary: { + // label: "Secondary", + // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // }, + // tertiary: { + // label: "Tertiary", + // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // } + // }} >

Start

{[...Array(50)].map((_el, i) => ( diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index 31b114b9..aa03e5d6 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -27,7 +27,12 @@ import { useIOTheme } from "../../core"; import { WithTestID } from "../../utils/types"; -import { ButtonLink, ButtonOutline, ButtonSolid } from "../buttons"; +import { + ButtonLink, + ButtonLinkProps, + ButtonOutline, + ButtonSolid +} from "../buttons"; import { VSpacer } from "../spacer"; type GradientScrollActions = @@ -52,9 +57,10 @@ type GradientScrollActions = type GradientScrollView = WithTestID< PropsWithChildren<{ - actionsProps: GradientScrollActions; - excludeSafeAreaMargins?: boolean; + actionsProps?: GradientScrollActions; debugMode?: boolean; + /* Don't include safe area insets */ + excludeSafeAreaMargins?: boolean; }> >; @@ -92,19 +98,18 @@ const styles = StyleSheet.create({ export const GradientScrollView = ({ children, - actionsProps: { - type, - primary: primaryAction, - secondary: secondaryAction, - tertiary: tertiaryAction - }, - // Don't include safe area insets + actionsProps, excludeSafeAreaMargins = false, debugMode = false, testID }: GradientScrollView) => { const theme = useIOTheme(); + const type = actionsProps?.type; + const primaryAction = actionsProps?.primary; + const secondaryAction = actionsProps?.secondary; + const tertiaryAction = actionsProps?.tertiary; + /* Shared Values for `reanimated` */ const scrollPositionPercentage = useSharedValue(0); /* Scroll position */ @@ -201,102 +206,108 @@ export const GradientScrollView = ({ scrollEventThrottle={8} contentContainerStyle={{ paddingHorizontal: IOVisualCostants.appMarginDefault, - paddingBottom: safeBottomAreaHeight + paddingBottom: actionsProps + ? safeBottomAreaHeight + : bottomMargin + contentEndMargin }} > {children} - - - - + + + - {/* Safe background block. It's added because when you swipe up + {/* 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. */} - - - - - {primaryAction && } - - {type === "TwoButtons" && secondaryAction && ( - - - - )} + /> + + + + {primaryAction && } + + {type === "TwoButtons" && secondaryAction && ( + + + + + )} - {type === "ThreeButtons" && ( - <> - {secondaryAction && ( - <> - - - - )} - {tertiaryAction && ( - - - - - )} - - )} + {type === "ThreeButtons" && ( + <> + {secondaryAction && ( + <> + + + + )} + {tertiaryAction && ( + + + + + )} + + )} + - + )} ); }; From 0616d82b137165be60dcdcc40a61c89a55f138d3 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Thu, 18 Apr 2024 10:20:37 +0200 Subject: [PATCH 13/15] Restore `GradientScrollView`, add new refactored `IOScrollView` instead --- example/src/navigation/navigator.tsx | 16 +- example/src/navigation/params.ts | 1 + example/src/navigation/routes.ts | 4 + example/src/pages/GradientScroll.tsx | 97 +++-- example/src/pages/IOScrollViewScreen.tsx | 68 ++++ .../layout/GradientBottomActions.tsx | 144 ++++++++ src/components/layout/GradientScrollView.tsx | 347 +++++------------- src/components/layout/IOScrollView.tsx | 310 ++++++++++++++++ src/components/layout/index.tsx | 2 + 9 files changed, 685 insertions(+), 304 deletions(-) create mode 100644 example/src/pages/IOScrollViewScreen.tsx create mode 100644 src/components/layout/GradientBottomActions.tsx create mode 100644 src/components/layout/IOScrollView.tsx diff --git a/example/src/navigation/navigator.tsx b/example/src/navigation/navigator.tsx index e57f423c..7cd153f7 100644 --- a/example/src/navigation/navigator.tsx +++ b/example/src/navigation/navigator.tsx @@ -1,18 +1,18 @@ import { HeaderSecondLevel, - ModalBSHeader, IOStyles, IOThemeDark, IOThemeLight, + ModalBSHeader, useIOThemeContext } from "@pagopa/io-app-design-system"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import React from "react"; import { DarkTheme, DefaultTheme, NavigationContainer } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import React from "react"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { Accordion } from "../pages/Accordion"; import { DSAdvice } from "../pages/Advice"; @@ -28,6 +28,7 @@ import { GradientScroll } from "../pages/GradientScroll"; import { HeaderFirstLevelScreen } from "../pages/HeaderFirstLevel"; import { HeaderSecondLevelScreen } from "../pages/HeaderSecondLevel"; import { HeaderSecondLevelWithStepper } from "../pages/HeaderSecondLevelWithStepper"; +import { IOScrollViewScreen } from "../pages/IOScrollViewScreen"; import { Icons } from "../pages/Icons"; import { ImageScreen } from "../pages/Image"; import { Layout } from "../pages/Layout"; @@ -382,6 +383,15 @@ const AppNavigator = () => { }} /> + + { - const theme = useIOTheme(); - - return ( - ( + + Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") }} > - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // }, - // secondary: { - // label: "Secondary", - // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // }, - // tertiary: { - // label: "Tertiary", - // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // } - // }} - > -

Start

- {[...Array(50)].map((_el, i) => ( - Repeated text - ))} - - - - Alert.alert("Test button")} - /> - {[...Array(2)].map((_el, i) => ( - Repeated text - ))} -

End

-
-
- ); -}; +

Start

+ {[...Array(50)].map((_el, i) => ( + Repeated text + ))} + + + + Alert.alert("Test button")} + /> + {[...Array(2)].map((_el, i) => ( + Repeated text + ))} +

End

+
+
+); diff --git a/example/src/pages/IOScrollViewScreen.tsx b/example/src/pages/IOScrollViewScreen.tsx new file mode 100644 index 00000000..738bb495 --- /dev/null +++ b/example/src/pages/IOScrollViewScreen.tsx @@ -0,0 +1,68 @@ +import { + Body, + ButtonOutline, + H2, + IOColors, + IOScrollView, + VSpacer, + useIOTheme +} from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { Alert, View } from "react-native"; + +export const IOScrollViewScreen = () => { + const theme = useIOTheme(); + + return ( + + Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + } + // secondary: { + // label: "Secondary", + // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // }, + // tertiary: { + // label: "Tertiary", + // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // } + }} + > +

Start

+ {[...Array(50)].map((_el, i) => ( + Repeated text + ))} + + + + Alert.alert("Test button")} + /> + {[...Array(2)].map((_el, i) => ( + Repeated text + ))} +

End

+
+
+ ); +}; diff --git a/src/components/layout/GradientBottomActions.tsx b/src/components/layout/GradientBottomActions.tsx new file mode 100644 index 00000000..325c98d4 --- /dev/null +++ b/src/components/layout/GradientBottomActions.tsx @@ -0,0 +1,144 @@ +import * as React from "react"; +import { Easing, StyleProp, StyleSheet, View, ViewStyle } from "react-native"; +import { easeGradient } from "react-native-easing-gradient"; +import LinearGradient from "react-native-linear-gradient"; +import Animated from "react-native-reanimated"; +import { IOColors, IOSpacer, IOVisualCostants, hexToRgba } from "../../core"; +import { WithTestID } from "../../utils/types"; +import { + ButtonLink, + ButtonLinkProps, + ButtonSolid, + ButtonSolidProps +} from "../buttons"; +import { VSpacer } from "../spacer"; + +export type GradientBottomActions = WithTestID<{ + transitionAnimStyle: Animated.AnimateStyle>; + dimensions: GradientBottomActionsDimensions; + // Accepted components: ButtonSolid for the primaryAction, ButtonLink for the secondaryAction + primaryActionProps?: Omit; + secondaryActionProps?: ButtonLinkProps; + // Debug mode + debugMode?: boolean; +}>; + +type GradientBottomActionsDimensions = { + bottomMargin: number; + extraBottomMargin: number; + gradientAreaHeight: number; + spaceBetweenActions: IOSpacer; + safeBackgroundHeight: number; +}; + +// Background color should be app main background (both light and dark themes) +const HEADER_BG_COLOR: IOColors = "white"; + +const { colors, locations } = easeGradient({ + colorStops: { + 0: { color: hexToRgba(IOColors[HEADER_BG_COLOR], 0) }, + 1: { color: IOColors[HEADER_BG_COLOR] } + }, + easing: Easing.ease, + extraColorStopsPerTransition: 20 +}); + +const styles = StyleSheet.create({ + buttonContainer: { + paddingHorizontal: IOVisualCostants.appMarginDefault, + width: "100%", + flex: 1, + flexShrink: 0, + justifyContent: "flex-end" + }, + gradientContainer: { + ...StyleSheet.absoluteFillObject + }, + safeBackgroundBlock: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + backgroundColor: IOColors[HEADER_BG_COLOR] + } +}); + +/** + * @deprecated This component has been included in the new `IOScrollView` after a proper refactor. It will be removed in a future release. + * @see IOScrollView + */ +export const GradientBottomActions = ({ + primaryActionProps: primaryAction, + secondaryActionProps: secondaryAction, + dimensions, + transitionAnimStyle, + debugMode, + testID +}: GradientBottomActions) => ( + + + {/* 100% opacity bg color fills at least 45% of the area */} + + + + + + + {primaryAction && ( + + )} + + {secondaryAction && ( + + + {} + + )} + + +); + +export default GradientBottomActions; diff --git a/src/components/layout/GradientScrollView.tsx b/src/components/layout/GradientScrollView.tsx index aa03e5d6..3170d4f7 100644 --- a/src/components/layout/GradientScrollView.tsx +++ b/src/components/layout/GradientScrollView.tsx @@ -1,201 +1,142 @@ import * as React from "react"; -import { ComponentProps, PropsWithChildren, useMemo, useState } from "react"; -import { - ColorValue, - LayoutChangeEvent, - LayoutRectangle, - StyleSheet, - View -} from "react-native"; -import { easeGradient } from "react-native-easing-gradient"; -import LinearGradient from "react-native-linear-gradient"; +import { useMemo } from "react"; import Animated, { Easing, - Extrapolate, - interpolate, useAnimatedScrollHandler, useAnimatedStyle, - useSharedValue + useSharedValue, + withTiming } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { - IOColors, IOSpacer, IOSpacingScale, IOVisualCostants, - hexToRgba, - useIOTheme + buttonSolidHeight } from "../../core"; import { WithTestID } from "../../utils/types"; -import { - ButtonLink, - ButtonLinkProps, - ButtonOutline, - ButtonSolid -} from "../buttons"; -import { VSpacer } from "../spacer"; - -type GradientScrollActions = - | { - type: "SingleButton"; - primary: Omit, "fullWidth">; - secondary?: never; - tertiary?: never; - } - | { - type: "TwoButtons"; - primary: Omit, "fullWidth">; - secondary: ComponentProps; - tertiary?: never; - } - | { - type: "ThreeButtons"; - primary: Omit, "fullWidth">; - secondary: Omit, "fullWidth">; - tertiary: ComponentProps; - }; - -type GradientScrollView = WithTestID< - PropsWithChildren<{ - actionsProps?: GradientScrollActions; - debugMode?: boolean; - /* Don't include safe area insets */ - excludeSafeAreaMargins?: boolean; - }> ->; - -/* Percentage of scrolled content that triggers - the gradient opaciy transition */ -const gradientOpacityScrollTrigger = 0.85; -/* Extended gradient area above the actions */ -const gradientSafeAreaHeight: IOSpacingScale = 96; -/* End content margin before the actions */ +import GradientBottomActions from "./GradientBottomActions"; + +export type GradientScrollView = WithTestID<{ + children: React.ReactNode; + excludeSafeAreaMargins?: boolean; + debugMode?: boolean; + // Accepted components: ButtonSolid, ButtonLink + // Don't use any components other than this, please. + primaryActionProps: GradientBottomActions["primaryActionProps"]; + secondaryActionProps?: GradientBottomActions["secondaryActionProps"]; +}>; + +// Extended gradient area above the actions +export const gradientSafeArea: IOSpacingScale = 96; +// End content margin before the actions const contentEndMargin: IOSpacingScale = 32; -/* Margin between ButtonSolid and ButtonOutline */ -const spaceBetweenActions: IOSpacer = 8; -/* Margin between ButtonSolid and ButtonLink */ -const spaceBetweenActionAndLink: IOSpacer = 16; -/* Extra bottom margin for iPhone bottom handle because - ButtonLink doesn't have a fixed height */ +// Margin between primary action and secondary one +const spaceBetweenActions: IOSpacer = 24; +// Estimated height of the secondary action +const secondaryActionEstHeight: number = 20; +// Extra bottom margin for iPhone bottom handle const extraSafeAreaMargin: IOSpacingScale = 8; -const styles = StyleSheet.create({ - gradientBottomActions: { - width: "100%", - position: "absolute", - bottom: 0, - justifyContent: "flex-end" - }, - buttonContainer: { - paddingHorizontal: IOVisualCostants.appMarginDefault, - width: "100%", - flexShrink: 0 - }, - gradientContainer: { - ...StyleSheet.absoluteFillObject - } -}); - +/** + * @deprecated This component has been deprecated. It will be removed in a future release. + * @see IOScrollView + */ export const GradientScrollView = ({ children, - actionsProps, + primaryActionProps: primaryActionProps, + secondaryActionProps: secondaryActionProps, + // Don't include safe area insets excludeSafeAreaMargins = false, debugMode = false, testID }: GradientScrollView) => { - const theme = useIOTheme(); - - const type = actionsProps?.type; - const primaryAction = actionsProps?.primary; - const secondaryAction = actionsProps?.secondary; - const tertiaryAction = actionsProps?.tertiary; - - /* Shared Values for `reanimated` */ - const scrollPositionPercentage = useSharedValue(0); /* Scroll position */ - - /* Total height of actions */ - const [actionBlockHeight, setActionBlockHeight] = - useState(0); - - const getActionBlockHeight = (event: LayoutChangeEvent) => { - setActionBlockHeight(event.nativeEvent.layout.height); - }; - + const gradientOpacity = useSharedValue(1); 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. */ + 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 + insets.bottom === 0 || excludeSafeAreaMargins ? IOVisualCostants.appMarginDefault - : safeAreaMargin, - [needSafeAreaMargin, excludeSafeAreaMargins, safeAreaMargin] + : insets.bottom, + [insets, excludeSafeAreaMargins] ); - /* GENERATE EASING GRADIENT - Background color should be app main background - (both light and dark themes) */ - const HEADER_BG_COLOR: ColorValue = IOColors[theme["appBackground-primary"]]; - - const { colors, locations } = easeGradient({ - colorStops: { - 0: { color: hexToRgba(HEADER_BG_COLOR, 0) }, - 1: { color: HEADER_BG_COLOR } - }, - easing: Easing.ease, - extraColorStopsPerTransition: 20 - }); - /* When the secondary action is visible, add extra margin - to avoid little space from iPhone bottom handle */ +to avoid little space from iPhone bottom handle */ const extraBottomMargin: number = useMemo( - () => (secondaryAction && needSafeAreaMargin ? extraSafeAreaMargin : 0), - [needSafeAreaMargin, secondaryAction] + () => + secondaryActionProps && insets.bottom !== 0 ? extraSafeAreaMargin : 0, + [insets.bottom, secondaryActionProps] ); - /* 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] + /* Total height of actions */ + const actionsArea: number = useMemo( + () => + primaryActionProps && secondaryActionProps + ? (buttonSolidHeight as number) + + spaceBetweenActions + + secondaryActionEstHeight + + extraBottomMargin + : buttonSolidHeight, + [extraBottomMargin, primaryActionProps, secondaryActionProps] ); /* Total height of "Actions + Gradient" area */ const gradientAreaHeight: number = useMemo( - () => bottomMargin + actionBlockHeight + gradientSafeAreaHeight, - [actionBlockHeight, bottomMargin] + () => bottomMargin + actionsArea + gradientSafeArea, + [actionsArea, bottomMargin] ); /* Height of the safe bottom area, applied to the ScrollView: - Actions + Content end margin */ + Actions + Content end margin */ const safeBottomAreaHeight: number = useMemo( - () => bottomMargin + actionBlockHeight + contentEndMargin, - [actionBlockHeight, bottomMargin] + () => bottomMargin + actionsArea + contentEndMargin, + [actionsArea, bottomMargin] + ); + + { + /* 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. */ + } + + const safeBackgroundHeight = useMemo( + () => + secondaryActionProps + ? spaceBetweenActions + + secondaryActionEstHeight + + extraBottomMargin + + bottomMargin + : bottomMargin, + [bottomMargin, extraBottomMargin, secondaryActionProps] ); const handleScroll = useAnimatedScrollHandler( ({ contentOffset, layoutMeasurement, contentSize }) => { - const scrollPosition = contentOffset.y; - const maxScrollHeight = contentSize.height - layoutMeasurement.height; - const scrollPercentage = scrollPosition / maxScrollHeight; + /* We use Math.floor because decimals used on Android + devices never change the `isEndReached` boolean value. + We have more consistent behavior across platforms + if we round these calculations ¯\_(ツ)_/¯ */ + const isEndReached = + Math.floor(layoutMeasurement.height + contentOffset.y) >= + Math.floor(contentSize.height); // eslint-disable-next-line functional/immutable-data - scrollPositionPercentage.value = scrollPercentage; + gradientOpacity.value = isEndReached ? 0 : 1; } ); const opacityTransition = useAnimatedStyle(() => ({ - opacity: interpolate( - scrollPositionPercentage.value, - [0, gradientOpacityScrollTrigger, 1], - [1, 1, 0], - Extrapolate.CLAMP - ) + opacity: withTiming(gradientOpacity.value, { + duration: 200, + easing: Easing.ease + }) })); return ( @@ -203,111 +144,27 @@ export const GradientScrollView = ({ {children} - {actionsProps && ( - - - - - - - {/* 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. */} - - - - - {primaryAction && } - - {type === "TwoButtons" && secondaryAction && ( - - - - - )} - - {type === "ThreeButtons" && ( - <> - {secondaryAction && ( - <> - - - - )} - {tertiaryAction && ( - - - - - )} - - )} - - - )} + ); }; diff --git a/src/components/layout/IOScrollView.tsx b/src/components/layout/IOScrollView.tsx new file mode 100644 index 00000000..72baabc5 --- /dev/null +++ b/src/components/layout/IOScrollView.tsx @@ -0,0 +1,310 @@ +import * as React from "react"; +import { ComponentProps, PropsWithChildren, useMemo, useState } from "react"; +import { + ColorValue, + LayoutChangeEvent, + LayoutRectangle, + StyleSheet, + View +} from "react-native"; +import { easeGradient } from "react-native-easing-gradient"; +import LinearGradient from "react-native-linear-gradient"; +import Animated, { + Easing, + Extrapolate, + interpolate, + useAnimatedScrollHandler, + useAnimatedStyle, + useSharedValue +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { + IOColors, + IOSpacer, + IOSpacingScale, + IOVisualCostants, + hexToRgba, + useIOTheme +} from "../../core"; +import { WithTestID } from "../../utils/types"; +import { ButtonLink, ButtonOutline, ButtonSolid } from "../buttons"; +import { VSpacer } from "../spacer"; + +type IOScrollViewActions = + | { + type: "SingleButton"; + primary: Omit, "fullWidth">; + secondary?: never; + tertiary?: never; + } + | { + type: "TwoButtons"; + primary: Omit, "fullWidth">; + secondary: ComponentProps; + tertiary?: never; + } + | { + type: "ThreeButtons"; + primary: Omit, "fullWidth">; + secondary: Omit, "fullWidth">; + tertiary: ComponentProps; + }; + +type IOScrollView = WithTestID< + PropsWithChildren<{ + actions?: IOScrollViewActions; + debugMode?: boolean; + /* Don't include safe area insets */ + excludeSafeAreaMargins?: boolean; + }> +>; + +/* Percentage of scrolled content that triggers + the gradient opaciy transition */ +const gradientOpacityScrollTrigger = 0.85; +/* Extended gradient area above the actions */ +const gradientSafeAreaHeight: IOSpacingScale = 96; +/* End content margin before the actions */ +const contentEndMargin: IOSpacingScale = 32; +/* Margin between ButtonSolid and ButtonOutline */ +const spaceBetweenActions: IOSpacer = 8; +/* Margin between ButtonSolid and ButtonLink */ +const spaceBetweenActionAndLink: IOSpacer = 16; +/* Extra bottom margin for iPhone bottom handle because + ButtonLink doesn't have a fixed height */ +const extraSafeAreaMargin: IOSpacingScale = 8; + +const styles = StyleSheet.create({ + gradientBottomActions: { + width: "100%", + position: "absolute", + bottom: 0, + justifyContent: "flex-end" + }, + gradientContainer: { + ...StyleSheet.absoluteFillObject + }, + buttonContainer: { + paddingHorizontal: IOVisualCostants.appMarginDefault, + width: "100%", + flexShrink: 0 + } +}); + +export const IOScrollView = ({ + children, + actions, + excludeSafeAreaMargins = false, + debugMode = false, + testID +}: IOScrollView) => { + const theme = useIOTheme(); + + const type = actions?.type; + const primaryAction = actions?.primary; + const secondaryAction = actions?.secondary; + const tertiaryAction = actions?.tertiary; + + /* Shared Values for `reanimated` */ + const scrollPositionPercentage = useSharedValue(0); /* Scroll position */ + + /* Total height of actions */ + const [actionBlockHeight, setActionBlockHeight] = + useState(0); + + const getActionBlockHeight = (event: LayoutChangeEvent) => { + setActionBlockHeight(event.nativeEvent.layout.height); + }; + + 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] + ); + + /* GENERATE EASING GRADIENT + Background color should be app main background + (both light and dark themes) */ + const HEADER_BG_COLOR: ColorValue = IOColors[theme["appBackground-primary"]]; + + const { colors, locations } = easeGradient({ + colorStops: { + 0: { color: hexToRgba(HEADER_BG_COLOR, 0) }, + 1: { color: HEADER_BG_COLOR } + }, + easing: Easing.ease, + extraColorStopsPerTransition: 20 + }); + + /* 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 at least 85% of the space + to avoid glitchy elements underneath */ + const safeBackgroundBlockHeight: number = useMemo( + () => (bottomMargin + actionBlockHeight) * 0.85, + [actionBlockHeight, bottomMargin] + ); + + /* Total height of "Actions + Gradient" area */ + const gradientAreaHeight: number = useMemo( + () => bottomMargin + actionBlockHeight + gradientSafeAreaHeight, + [actionBlockHeight, bottomMargin] + ); + + /* Height of the safe bottom area, applied to the ScrollView: + Actions + Content end margin */ + const safeBottomAreaHeight: number = useMemo( + () => bottomMargin + actionBlockHeight + contentEndMargin, + [actionBlockHeight, bottomMargin] + ); + + const handleScroll = useAnimatedScrollHandler( + ({ contentOffset, layoutMeasurement, contentSize }) => { + const scrollPosition = contentOffset.y; + const maxScrollHeight = contentSize.height - layoutMeasurement.height; + const scrollPercentage = scrollPosition / maxScrollHeight; + + // eslint-disable-next-line functional/immutable-data + scrollPositionPercentage.value = scrollPercentage; + } + ); + + const opacityTransition = useAnimatedStyle(() => ({ + opacity: interpolate( + scrollPositionPercentage.value, + [0, gradientOpacityScrollTrigger, 1], + [1, 1, 0], + Extrapolate.CLAMP + ) + })); + + return ( + <> + + {children} + + {actions && ( + + + + + + + {/* 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. */} + + + + + {primaryAction && } + + {type === "TwoButtons" && secondaryAction && ( + + + )} + /> + + )} + + {type === "ThreeButtons" && ( + <> + {secondaryAction && ( + <> + + + + )} + {tertiaryAction && ( + + + + + )} + + )} + + + )} + + ); +}; diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 4e312c6e..38b693fc 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -1,8 +1,10 @@ export * from "./BlockButtons"; export * from "./FooterWithButtons"; export * from "./ForceScrollDownView"; +export * from "./GradientBottomActions"; export * from "./GradientScrollView"; export * from "./HeaderFirstLevel"; export * from "./HeaderSecondLevel"; +export * from "./IOScrollView"; export * from "./ModalBSHeader"; export * from "./common"; From 0d4c0281b0b64375a43a7fa87226978d44a88454 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Thu, 18 Apr 2024 10:51:21 +0200 Subject: [PATCH 14/15] Remove `GradientScroll` from example app --- example/src/navigation/navigator.tsx | 14 +-- example/src/navigation/params.ts | 2 +- example/src/navigation/routes.ts | 8 +- example/src/pages/GradientScroll.tsx | 53 ----------- example/src/pages/IOScrollViewScreen.tsx | 87 +++++++++---------- .../IOScrollViewScreenWithoutActions.tsx | 23 +++++ src/components/layout/IOScrollView.tsx | 32 ++++--- 7 files changed, 96 insertions(+), 123 deletions(-) delete mode 100644 example/src/pages/GradientScroll.tsx create mode 100644 example/src/pages/IOScrollViewScreenWithoutActions.tsx diff --git a/example/src/navigation/navigator.tsx b/example/src/navigation/navigator.tsx index 7cd153f7..9f881e93 100644 --- a/example/src/navigation/navigator.tsx +++ b/example/src/navigation/navigator.tsx @@ -24,7 +24,6 @@ import { EndOfPageScreen, EndOfPageScreenWithCTA } from "../pages/EndOfPage"; import { FooterWithButton } from "../pages/FooterWithButton"; import { FooterWithButtonEmptyState } from "../pages/FooterWithButtonEmptyState"; import { ForceScrollDownViewPage } from "../pages/ForceScrollDownViewPage"; -import { GradientScroll } from "../pages/GradientScroll"; import { HeaderFirstLevelScreen } from "../pages/HeaderFirstLevel"; import { HeaderSecondLevelScreen } from "../pages/HeaderSecondLevel"; import { HeaderSecondLevelWithStepper } from "../pages/HeaderSecondLevelWithStepper"; @@ -49,6 +48,7 @@ import { TabNavigationScreen } from "../pages/TabNavigation"; import { TextInputs } from "../pages/TextInputs"; import { Toasts } from "../pages/Toasts"; import { Typography } from "../pages/Typography"; +import { IOScrollViewScreenWithoutActions } from "../pages/IOScrollViewScreenWithoutActions"; import { AppParamsList } from "./params"; import APP_ROUTES from "./routes"; @@ -375,19 +375,19 @@ const AppNavigator = () => { /> diff --git a/example/src/navigation/params.ts b/example/src/navigation/params.ts index 404f43c6..dbb6861a 100644 --- a/example/src/navigation/params.ts +++ b/example/src/navigation/params.ts @@ -32,8 +32,8 @@ export type AppParamsList = { [DESIGN_SYSTEM_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_STEPPER .route]: undefined; [DESIGN_SYSTEM_ROUTES.SCREENS.FOOTER_WITH_BUTTON.route]: undefined; - [DESIGN_SYSTEM_ROUTES.SCREENS.GRADIENT_SCROLLVIEW.route]: undefined; [DESIGN_SYSTEM_ROUTES.SCREENS.IOSCROLLVIEW.route]: undefined; + [DESIGN_SYSTEM_ROUTES.SCREENS.IOSCROLLVIEW_WO_ACTIONS.route]: undefined; [DESIGN_SYSTEM_ROUTES.SCREENS.FOOTER_WITH_BUTTON_EMPTY.route]: undefined; [DESIGN_SYSTEM_ROUTES.COMPONENTS.TOASTS.route]: undefined; [DESIGN_SYSTEM_ROUTES.SCREENS.FULL_SCREEN_MODAL.route]: undefined; diff --git a/example/src/navigation/routes.ts b/example/src/navigation/routes.ts index 01f09fba..ee8072c5 100644 --- a/example/src/navigation/routes.ts +++ b/example/src/navigation/routes.ts @@ -78,13 +78,13 @@ const APP_ROUTES = { route: "DESIGN_SYSTEM_FOOTER_WITH_BUTTON", title: "Footer with button" }, - GRADIENT_SCROLLVIEW: { - route: "DESIGN_SYSTEM_GRADIENT_SCROLLVIEW", - title: "Gradient ScrollView" - }, IOSCROLLVIEW: { route: "DS_IOSCROLLVIEW", title: "IO ScrollView" + }, + IOSCROLLVIEW_WO_ACTIONS: { + route: "DS_IOSCROLLVIEW_WO_ACTIONS", + title: "IO ScrollView (w/o actions)" } }, SANDBOX: { diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx deleted file mode 100644 index 32f3acb4..00000000 --- a/example/src/pages/GradientScroll.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - Body, - ButtonOutline, - GradientScrollView, - H2, - IOColors, - VSpacer -} from "@pagopa/io-app-design-system"; -import * as React from "react"; -import { Alert, View } from "react-native"; - -export const GradientScroll = () => ( - - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - }} - > -

Start

- {[...Array(50)].map((_el, i) => ( - Repeated text - ))} - - - - Alert.alert("Test button")} - /> - {[...Array(2)].map((_el, i) => ( - Repeated text - ))} -

End

-
-
-); diff --git a/example/src/pages/IOScrollViewScreen.tsx b/example/src/pages/IOScrollViewScreen.tsx index 738bb495..f46c8138 100644 --- a/example/src/pages/IOScrollViewScreen.tsx +++ b/example/src/pages/IOScrollViewScreen.tsx @@ -14,55 +14,48 @@ export const IOScrollViewScreen = () => { const theme = useIOTheme(); return ( - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + } + // secondary: { + // label: "Secondary", + // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // }, + // tertiary: { + // label: "Tertiary", + // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + // } }} > - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - } - // secondary: { - // label: "Secondary", - // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // }, - // tertiary: { - // label: "Tertiary", - // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // } +

Start

+ {[...Array(50)].map((_el, i) => ( + Repeated text + ))} + + -

Start

- {[...Array(50)].map((_el, i) => ( - Repeated text - ))} - - - - Alert.alert("Test button")} - /> - {[...Array(2)].map((_el, i) => ( - Repeated text - ))} -

End

-
-
+ /> + + Alert.alert("Test button")} + /> + {[...Array(2)].map((_el, i) => ( + Repeated text + ))} +

End

+ ); }; diff --git a/example/src/pages/IOScrollViewScreenWithoutActions.tsx b/example/src/pages/IOScrollViewScreenWithoutActions.tsx new file mode 100644 index 00000000..2e21b8af --- /dev/null +++ b/example/src/pages/IOScrollViewScreenWithoutActions.tsx @@ -0,0 +1,23 @@ +import { + Body, + H2, + IOScrollView, + VSpacer, + useIOTheme +} from "@pagopa/io-app-design-system"; +import * as React from "react"; + +export const IOScrollViewScreenWithoutActions = () => { + const theme = useIOTheme(); + + return ( + +

Start

+ {[...Array(50)].map((_el, i) => ( + Repeated text + ))} + +

End

+
+ ); +}; diff --git a/src/components/layout/IOScrollView.tsx b/src/components/layout/IOScrollView.tsx index 72baabc5..19e21c1c 100644 --- a/src/components/layout/IOScrollView.tsx +++ b/src/components/layout/IOScrollView.tsx @@ -1,5 +1,11 @@ import * as React from "react"; -import { ComponentProps, PropsWithChildren, useMemo, useState } from "react"; +import { + ComponentProps, + Fragment, + PropsWithChildren, + useMemo, + useState +} from "react"; import { ColorValue, LayoutChangeEvent, @@ -194,12 +200,13 @@ export const IOScrollView = ({ })); return ( - <> + {primaryAction && } - {type === "TwoButtons" && secondaryAction && ( + {type === "TwoButtons" && ( - )} - /> + {secondaryAction && ( + )} + /> + )} )} {type === "ThreeButtons" && ( - <> + {secondaryAction && ( - <> + - + )} + {tertiaryAction && ( )} - + )}
)} - + ); }; From 6e0d5f0a9e389e1ad568efd43f68975218232d4c Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Thu, 18 Apr 2024 17:55:42 +0200 Subject: [PATCH 15/15] Restore previous configuration --- example/src/navigation/navigator.tsx | 18 +- example/src/navigation/params.ts | 3 +- example/src/navigation/routes.ts | 10 +- example/src/pages/GradientScroll.tsx | 53 +++ example/src/pages/IOScrollViewScreen.tsx | 61 ---- .../IOScrollViewScreenWithoutActions.tsx | 23 -- src/components/layout/IOScrollView.tsx | 320 ------------------ src/components/layout/index.tsx | 1 - 8 files changed, 61 insertions(+), 428 deletions(-) create mode 100644 example/src/pages/GradientScroll.tsx delete mode 100644 example/src/pages/IOScrollViewScreen.tsx delete mode 100644 example/src/pages/IOScrollViewScreenWithoutActions.tsx delete mode 100644 src/components/layout/IOScrollView.tsx diff --git a/example/src/navigation/navigator.tsx b/example/src/navigation/navigator.tsx index 9f881e93..d846c298 100644 --- a/example/src/navigation/navigator.tsx +++ b/example/src/navigation/navigator.tsx @@ -24,10 +24,10 @@ import { EndOfPageScreen, EndOfPageScreenWithCTA } from "../pages/EndOfPage"; import { FooterWithButton } from "../pages/FooterWithButton"; import { FooterWithButtonEmptyState } from "../pages/FooterWithButtonEmptyState"; import { ForceScrollDownViewPage } from "../pages/ForceScrollDownViewPage"; +import { GradientScroll } from "../pages/GradientScroll"; import { HeaderFirstLevelScreen } from "../pages/HeaderFirstLevel"; import { HeaderSecondLevelScreen } from "../pages/HeaderSecondLevel"; import { HeaderSecondLevelWithStepper } from "../pages/HeaderSecondLevelWithStepper"; -import { IOScrollViewScreen } from "../pages/IOScrollViewScreen"; import { Icons } from "../pages/Icons"; import { ImageScreen } from "../pages/Image"; import { Layout } from "../pages/Layout"; @@ -48,7 +48,6 @@ import { TabNavigationScreen } from "../pages/TabNavigation"; import { TextInputs } from "../pages/TextInputs"; import { Toasts } from "../pages/Toasts"; import { Typography } from "../pages/Typography"; -import { IOScrollViewScreenWithoutActions } from "../pages/IOScrollViewScreenWithoutActions"; import { AppParamsList } from "./params"; import APP_ROUTES from "./routes"; @@ -375,19 +374,10 @@ const AppNavigator = () => { /> - - diff --git a/example/src/navigation/params.ts b/example/src/navigation/params.ts index dbb6861a..e4e556cb 100644 --- a/example/src/navigation/params.ts +++ b/example/src/navigation/params.ts @@ -32,8 +32,7 @@ export type AppParamsList = { [DESIGN_SYSTEM_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_STEPPER .route]: undefined; [DESIGN_SYSTEM_ROUTES.SCREENS.FOOTER_WITH_BUTTON.route]: undefined; - [DESIGN_SYSTEM_ROUTES.SCREENS.IOSCROLLVIEW.route]: undefined; - [DESIGN_SYSTEM_ROUTES.SCREENS.IOSCROLLVIEW_WO_ACTIONS.route]: undefined; + [DESIGN_SYSTEM_ROUTES.SCREENS.GRADIENT_SCROLLVIEW.route]: undefined; [DESIGN_SYSTEM_ROUTES.SCREENS.FOOTER_WITH_BUTTON_EMPTY.route]: undefined; [DESIGN_SYSTEM_ROUTES.COMPONENTS.TOASTS.route]: undefined; [DESIGN_SYSTEM_ROUTES.SCREENS.FULL_SCREEN_MODAL.route]: undefined; diff --git a/example/src/navigation/routes.ts b/example/src/navigation/routes.ts index ee8072c5..6a299358 100644 --- a/example/src/navigation/routes.ts +++ b/example/src/navigation/routes.ts @@ -78,13 +78,9 @@ const APP_ROUTES = { route: "DESIGN_SYSTEM_FOOTER_WITH_BUTTON", title: "Footer with button" }, - IOSCROLLVIEW: { - route: "DS_IOSCROLLVIEW", - title: "IO ScrollView" - }, - IOSCROLLVIEW_WO_ACTIONS: { - route: "DS_IOSCROLLVIEW_WO_ACTIONS", - title: "IO ScrollView (w/o actions)" + GRADIENT_SCROLLVIEW: { + route: "DESIGN_SYSTEM_GRADIENT_SCROLLVIEW", + title: "Gradient ScrollView" } }, SANDBOX: { diff --git a/example/src/pages/GradientScroll.tsx b/example/src/pages/GradientScroll.tsx new file mode 100644 index 00000000..32f3acb4 --- /dev/null +++ b/example/src/pages/GradientScroll.tsx @@ -0,0 +1,53 @@ +import { + Body, + ButtonOutline, + GradientScrollView, + H2, + IOColors, + VSpacer +} from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { Alert, View } from "react-native"; + +export const GradientScroll = () => ( + + Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") + }} + > +

Start

+ {[...Array(50)].map((_el, i) => ( + Repeated text + ))} + + + + Alert.alert("Test button")} + /> + {[...Array(2)].map((_el, i) => ( + Repeated text + ))} +

End

+
+
+); diff --git a/example/src/pages/IOScrollViewScreen.tsx b/example/src/pages/IOScrollViewScreen.tsx deleted file mode 100644 index f46c8138..00000000 --- a/example/src/pages/IOScrollViewScreen.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { - Body, - ButtonOutline, - H2, - IOColors, - IOScrollView, - VSpacer, - useIOTheme -} from "@pagopa/io-app-design-system"; -import * as React from "react"; -import { Alert, View } from "react-native"; - -export const IOScrollViewScreen = () => { - const theme = useIOTheme(); - - return ( - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - } - // secondary: { - // label: "Secondary", - // onPress: () => Alert.alert("Secondary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // }, - // tertiary: { - // label: "Tertiary", - // onPress: () => Alert.alert("Tertiary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - // } - }} - > -

Start

- {[...Array(50)].map((_el, i) => ( - Repeated text - ))} - - - - Alert.alert("Test button")} - /> - {[...Array(2)].map((_el, i) => ( - Repeated text - ))} -

End

-
- ); -}; diff --git a/example/src/pages/IOScrollViewScreenWithoutActions.tsx b/example/src/pages/IOScrollViewScreenWithoutActions.tsx deleted file mode 100644 index 2e21b8af..00000000 --- a/example/src/pages/IOScrollViewScreenWithoutActions.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { - Body, - H2, - IOScrollView, - VSpacer, - useIOTheme -} from "@pagopa/io-app-design-system"; -import * as React from "react"; - -export const IOScrollViewScreenWithoutActions = () => { - const theme = useIOTheme(); - - return ( - -

Start

- {[...Array(50)].map((_el, i) => ( - Repeated text - ))} - -

End

-
- ); -}; diff --git a/src/components/layout/IOScrollView.tsx b/src/components/layout/IOScrollView.tsx deleted file mode 100644 index 19e21c1c..00000000 --- a/src/components/layout/IOScrollView.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import * as React from "react"; -import { - ComponentProps, - Fragment, - PropsWithChildren, - useMemo, - useState -} from "react"; -import { - ColorValue, - LayoutChangeEvent, - LayoutRectangle, - StyleSheet, - View -} from "react-native"; -import { easeGradient } from "react-native-easing-gradient"; -import LinearGradient from "react-native-linear-gradient"; -import Animated, { - Easing, - Extrapolate, - interpolate, - useAnimatedScrollHandler, - useAnimatedStyle, - useSharedValue -} from "react-native-reanimated"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { - IOColors, - IOSpacer, - IOSpacingScale, - IOVisualCostants, - hexToRgba, - useIOTheme -} from "../../core"; -import { WithTestID } from "../../utils/types"; -import { ButtonLink, ButtonOutline, ButtonSolid } from "../buttons"; -import { VSpacer } from "../spacer"; - -type IOScrollViewActions = - | { - type: "SingleButton"; - primary: Omit, "fullWidth">; - secondary?: never; - tertiary?: never; - } - | { - type: "TwoButtons"; - primary: Omit, "fullWidth">; - secondary: ComponentProps; - tertiary?: never; - } - | { - type: "ThreeButtons"; - primary: Omit, "fullWidth">; - secondary: Omit, "fullWidth">; - tertiary: ComponentProps; - }; - -type IOScrollView = WithTestID< - PropsWithChildren<{ - actions?: IOScrollViewActions; - debugMode?: boolean; - /* Don't include safe area insets */ - excludeSafeAreaMargins?: boolean; - }> ->; - -/* Percentage of scrolled content that triggers - the gradient opaciy transition */ -const gradientOpacityScrollTrigger = 0.85; -/* Extended gradient area above the actions */ -const gradientSafeAreaHeight: IOSpacingScale = 96; -/* End content margin before the actions */ -const contentEndMargin: IOSpacingScale = 32; -/* Margin between ButtonSolid and ButtonOutline */ -const spaceBetweenActions: IOSpacer = 8; -/* Margin between ButtonSolid and ButtonLink */ -const spaceBetweenActionAndLink: IOSpacer = 16; -/* Extra bottom margin for iPhone bottom handle because - ButtonLink doesn't have a fixed height */ -const extraSafeAreaMargin: IOSpacingScale = 8; - -const styles = StyleSheet.create({ - gradientBottomActions: { - width: "100%", - position: "absolute", - bottom: 0, - justifyContent: "flex-end" - }, - gradientContainer: { - ...StyleSheet.absoluteFillObject - }, - buttonContainer: { - paddingHorizontal: IOVisualCostants.appMarginDefault, - width: "100%", - flexShrink: 0 - } -}); - -export const IOScrollView = ({ - children, - actions, - excludeSafeAreaMargins = false, - debugMode = false, - testID -}: IOScrollView) => { - const theme = useIOTheme(); - - const type = actions?.type; - const primaryAction = actions?.primary; - const secondaryAction = actions?.secondary; - const tertiaryAction = actions?.tertiary; - - /* Shared Values for `reanimated` */ - const scrollPositionPercentage = useSharedValue(0); /* Scroll position */ - - /* Total height of actions */ - const [actionBlockHeight, setActionBlockHeight] = - useState(0); - - const getActionBlockHeight = (event: LayoutChangeEvent) => { - setActionBlockHeight(event.nativeEvent.layout.height); - }; - - 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] - ); - - /* GENERATE EASING GRADIENT - Background color should be app main background - (both light and dark themes) */ - const HEADER_BG_COLOR: ColorValue = IOColors[theme["appBackground-primary"]]; - - const { colors, locations } = easeGradient({ - colorStops: { - 0: { color: hexToRgba(HEADER_BG_COLOR, 0) }, - 1: { color: HEADER_BG_COLOR } - }, - easing: Easing.ease, - extraColorStopsPerTransition: 20 - }); - - /* 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 at least 85% of the space - to avoid glitchy elements underneath */ - const safeBackgroundBlockHeight: number = useMemo( - () => (bottomMargin + actionBlockHeight) * 0.85, - [actionBlockHeight, bottomMargin] - ); - - /* Total height of "Actions + Gradient" area */ - const gradientAreaHeight: number = useMemo( - () => bottomMargin + actionBlockHeight + gradientSafeAreaHeight, - [actionBlockHeight, bottomMargin] - ); - - /* Height of the safe bottom area, applied to the ScrollView: - Actions + Content end margin */ - const safeBottomAreaHeight: number = useMemo( - () => bottomMargin + actionBlockHeight + contentEndMargin, - [actionBlockHeight, bottomMargin] - ); - - const handleScroll = useAnimatedScrollHandler( - ({ contentOffset, layoutMeasurement, contentSize }) => { - const scrollPosition = contentOffset.y; - const maxScrollHeight = contentSize.height - layoutMeasurement.height; - const scrollPercentage = scrollPosition / maxScrollHeight; - - // eslint-disable-next-line functional/immutable-data - scrollPositionPercentage.value = scrollPercentage; - } - ); - - const opacityTransition = useAnimatedStyle(() => ({ - opacity: interpolate( - scrollPositionPercentage.value, - [0, gradientOpacityScrollTrigger, 1], - [1, 1, 0], - Extrapolate.CLAMP - ) - })); - - return ( - - - {children} - - {actions && ( - - - - - - - {/* 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. */} - - - - - {primaryAction && } - - {type === "TwoButtons" && ( - - - {secondaryAction && ( - )} - /> - )} - - )} - - {type === "ThreeButtons" && ( - - {secondaryAction && ( - - - - - )} - - {tertiaryAction && ( - - - - - )} - - )} - - - )} - - ); -}; diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 38b693fc..4b9de220 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -5,6 +5,5 @@ export * from "./GradientBottomActions"; export * from "./GradientScrollView"; export * from "./HeaderFirstLevel"; export * from "./HeaderSecondLevel"; -export * from "./IOScrollView"; export * from "./ModalBSHeader"; export * from "./common";