Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore(Cross): [IOAPPX-283] Add IOScrollView (next iteration of GradientScroll, now deprecated) + IOScrollViewWithLargeHeader #5704

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
17943aa
Add first iteration of `IOScrollView`
dmnplb Apr 18, 2024
54db6f9
Fix `IOScrollViewWithLargeHeader` behavior
dmnplb Apr 18, 2024
db23087
Add deprecation note to `RNavScreenWithLargeHeader`
dmnplb Apr 18, 2024
1d59280
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb Apr 18, 2024
f0a3f3f
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb Apr 18, 2024
b464f92
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb Apr 22, 2024
ffcd380
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
CrisTofani Apr 22, 2024
05e2170
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb Apr 23, 2024
c6e13ed
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb Apr 24, 2024
2eecd86
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
Ladirico May 3, 2024
5453671
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
CrisTofani May 6, 2024
fbeb2b9
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
CrisTofani May 8, 2024
1231cb3
Update ts/components/ui/IOScrollView.tsx
dmnplb May 8, 2024
aa33ce1
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 8, 2024
4977b21
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
hevelius May 9, 2024
303e246
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 13, 2024
0486daf
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 14, 2024
f5ca80c
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 15, 2024
f3b8869
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 15, 2024
59604e7
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 17, 2024
c8f5b3f
Increase space between actions, according to design guidelines
dmnplb May 17, 2024
b3105a6
Update `DSIOScrollView` button labels
dmnplb May 17, 2024
27e6d4d
Bump `io-app-design-system` version
dmnplb May 17, 2024
d1f71e9
Update snapshots
dmnplb May 17, 2024
918ab97
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 17, 2024
763d43d
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 17, 2024
fb4f97f
Merge branch 'master' into IOAPPX-283-add-ioscrollview-next-iteration…
dmnplb May 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"dependencies": {
"@babel/plugin-transform-regenerator": "^7.18.6",
"@gorhom/bottom-sheet": "^4.1.5",
"@pagopa/io-app-design-system": "1.36.11",
"@pagopa/io-app-design-system": "1.36.14",
"@pagopa/io-pagopa-commons": "^3.1.0",
"@pagopa/io-react-native-crypto": "^0.3.0",
"@pagopa/io-react-native-http-client": "^0.1.3",
Expand Down Expand Up @@ -149,6 +149,7 @@
"react-native-crypto": "^2.1.0",
"react-native-device-info": "^10.8.0",
"react-native-document-picker": "^9.1.1",
"react-native-easing-gradient": "^1.1.1",
"react-native-exception-handler": "^2.10.8",
"react-native-fingerprint-scanner": "^6.0.0",
"react-native-flag-secure-android": "^1.0.3",
Expand Down
370 changes: 370 additions & 0 deletions ts/components/ui/IOScrollView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,370 @@
import {
ButtonLink,
ButtonOutline,
ButtonSolid,
HeaderSecondLevel,
IOColors,
IOSpacer,
IOSpacingScale,
IOVisualCostants,
VSpacer,
hexToRgba,
useIOTheme
} from "@pagopa/io-app-design-system";
import * as React from "react";
import {
ComponentProps,
Fragment,
PropsWithChildren,
useLayoutEffect,
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 { useNavigation } from "@react-navigation/native";
import { WithTestID } from "../../types/WithTestID";

type IOScrollViewActions =
| {
type: "SingleButton";
primary: Omit<ComponentProps<typeof ButtonSolid>, "fullWidth">;
secondary?: never;
tertiary?: never;
}
| {
type: "TwoButtons";
primary: Omit<ComponentProps<typeof ButtonSolid>, "fullWidth">;
secondary: ComponentProps<typeof ButtonLink>;
tertiary?: never;
}
| {
type: "ThreeButtons";
primary: Omit<ComponentProps<typeof ButtonSolid>, "fullWidth">;
secondary: Omit<ComponentProps<typeof ButtonOutline>, "fullWidth">;
tertiary: ComponentProps<typeof ButtonLink>;
};

type IOSCrollViewHeaderScrollValues = ComponentProps<
typeof HeaderSecondLevel
>["scrollValues"];

type IOScrollView = WithTestID<
PropsWithChildren<{
headerConfig?: ComponentProps<typeof HeaderSecondLevel>;
actions?: IOScrollViewActions;
debugMode?: boolean;
snapOffset?: number;
/* Don't include safe area insets */
excludeSafeAreaMargins?: boolean;
/* Include page margins */
includeContentMargins?: 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 = 16;
/* 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 = ({
headerConfig,
children,
actions,
snapOffset,
excludeSafeAreaMargins = false,
includeContentMargins = true,
debugMode = false,
testID
}: IOScrollView) => {
const theme = useIOTheme();

const type = actions?.type;
const primaryAction = actions?.primary;
const secondaryAction = actions?.secondary;
const tertiaryAction = actions?.tertiary;

/* Navigation */
const navigation = useNavigation();

/* Shared Values for `reanimated` */
const scrollPositionAbsolute =
useSharedValue(0); /* Scroll position (Absolute) */
const scrollPositionPercentage =
useSharedValue(0); /* Scroll position (Relative) */

/* Total height of actions */
const [actionBlockHeight, setActionBlockHeight] =
useState<LayoutRectangle["height"]>(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
scrollPositionAbsolute.value = scrollPosition;
// 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
)
}));

/* Set custom header with `react-navigation` library using
`useLayoutEffect` hook */

const scrollValues: IOSCrollViewHeaderScrollValues = useMemo(
() => ({
contentOffsetY: scrollPositionAbsolute,
triggerOffset: snapOffset || 0
}),
[scrollPositionAbsolute, snapOffset]
);

useLayoutEffect(() => {
if (headerConfig) {
navigation.setOptions({
header: () => (
<HeaderSecondLevel {...headerConfig} scrollValues={scrollValues} />
),
headerTransparent: headerConfig.transparent
});
}
}, [headerConfig, navigation, scrollValues]);

return (
<Fragment>
<Animated.ScrollView
testID={testID}
onScroll={handleScroll}
scrollEventThrottle={8}
snapToOffsets={[0, snapOffset || 0]}
snapToEnd={false}
decelerationRate="normal"
contentContainerStyle={{
paddingBottom: actions
? safeBottomAreaHeight
: bottomMargin + contentEndMargin,
paddingHorizontal: includeContentMargins
? IOVisualCostants.appMarginDefault
: 0
}}
>
{children}
</Animated.ScrollView>
{actions && (
<View
style={[
styles.gradientBottomActions,
{
height: gradientAreaHeight,
paddingBottom: bottomMargin
}
]}
testID={testID}
pointerEvents="box-none"
>
<Animated.View
style={[
styles.gradientContainer,
debugMode && {
backgroundColor: hexToRgba(IOColors["error-500"], 0.15)
}
]}
pointerEvents="none"
>
<Animated.View
style={[
opacityTransition,
debugMode && {
borderTopColor: IOColors["error-500"],
borderTopWidth: 1,
backgroundColor: hexToRgba(IOColors["error-500"], 0.4)
}
]}
>
<LinearGradient
style={{
height: gradientAreaHeight - safeBackgroundBlockHeight
}}
locations={locations}
colors={colors}
/>
</Animated.View>

{/* 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. */}
<View
style={{
bottom: 0,
height: safeBackgroundBlockHeight,
backgroundColor: HEADER_BG_COLOR
}}
/>
</Animated.View>

<View
style={styles.buttonContainer}
onLayout={getActionBlockHeight}
pointerEvents="box-none"
>
{primaryAction && <ButtonSolid fullWidth {...primaryAction} />}

{type === "TwoButtons" && (
<View
style={{
alignSelf: "center",
marginBottom: extraBottomMargin
}}
>
<VSpacer size={spaceBetweenActionAndLink} />
{secondaryAction && (
<ButtonLink
{...(secondaryAction as ComponentProps<typeof ButtonLink>)}
/>
)}
</View>
)}

{type === "ThreeButtons" && (
<Fragment>
{secondaryAction && (
<Fragment>
<VSpacer size={spaceBetweenActions} />
<ButtonOutline fullWidth {...secondaryAction} />
</Fragment>
)}

{tertiaryAction && (
<View
style={{
alignSelf: "center",
marginBottom: extraBottomMargin
}}
>
<VSpacer size={spaceBetweenActionAndLink} />
<ButtonLink {...tertiaryAction} />
</View>
)}
</Fragment>
)}
</View>
</View>
)}
</Fragment>
);
};