Skip to content

Commit

Permalink
[IOAPPX-365] Add enableDiscreteTransition to HeaderSecondLevel to…
Browse files Browse the repository at this point in the history
… allow transition even if `triggerOffset` is not set (#323)

## Short description
This PR adds the new `enableDiscreteTransition` option to
`HeaderSecondLevel` to allow an animated transition when `triggerOffset`
is not set, or in some specific cases where there is a custom background
and the default transition looks bad. The new transition differs from
the previous one in that it's always triggered when the user scrolls
down, even by one pixel. In the default transition you need to set a
custom height along with a proper `snapToOffsets` array.

## List of changes proposed in this pull request
- Add the new `enableDiscreteTransition` boolean prop along with the
`animatedRef` mandatory prop
- Add two new screens to the `example` app for demo purposes

### Preview
| Default | Discrete |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/64066269-74db-462d-8a40-284410e0328a"
/> | <video
src="https://github.com/user-attachments/assets/cd565eee-f97c-4cee-bb2f-921822c2e016"
/> |





#### Related PR
* pagopa/io-app#6054

## How to test
1. Launch the example app
2. Go to the **Header Second Level (discrete transition, …)** screens
  • Loading branch information
dmnplb authored Aug 27, 2024
1 parent 037242d commit 82f79fb
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 8 deletions.
30 changes: 30 additions & 0 deletions example/src/navigation/navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import { Toasts } from "../pages/Toasts";
import { Typography } from "../pages/Typography";
import { SearchCustom } from "../pages/SearchCustom";
import { HeaderSecondLevelCustomBackground } from "../pages/HeaderSecondLevelCustomBackground";
import { HeaderSecondLevelScreenDiscreteTransition } from "../pages/HeaderSecondLevelDiscreteTransition";
import { HeaderSecondLevelScreenDiscreteTransitionCustomBg } from "../pages/HeaderSecondLevelScreenDiscreteTransitionCustomBg";
import { AppParamsList } from "./params";
import APP_ROUTES from "./routes";

Expand Down Expand Up @@ -318,6 +320,34 @@ const AppNavigator = () => {
}}
/>

<Stack.Screen
name={
APP_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_DISCRETE_TRANSITION
.route
}
component={HeaderSecondLevelScreenDiscreteTransition}
options={{
headerTitle:
APP_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_DISCRETE_TRANSITION
.title,
headerBackTitleVisible: false
}}
/>

<Stack.Screen
name={
APP_ROUTES.COMPONENTS
.HEADER_SECOND_LEVEL_DISCRETE_TRANSITION_CUSTOM_BG.route
}
component={HeaderSecondLevelScreenDiscreteTransitionCustomBg}
options={{
headerTitle:
APP_ROUTES.COMPONENTS
.HEADER_SECOND_LEVEL_DISCRETE_TRANSITION_CUSTOM_BG.title,
headerBackTitleVisible: false
}}
/>

<Stack.Screen
name={APP_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_CUSTOM_BG.route}
component={HeaderSecondLevelCustomBackground}
Expand Down
4 changes: 4 additions & 0 deletions example/src/navigation/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export type AppParamsList = {
[DESIGN_SYSTEM_ROUTES.COMPONENTS.FORCE_SCROLL_DOWN.route]: undefined;
[DESIGN_SYSTEM_ROUTES.COMPONENTS.HEADER_FIRST_LEVEL.route]: undefined;
[DESIGN_SYSTEM_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL.route]: undefined;
[DESIGN_SYSTEM_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_DISCRETE_TRANSITION
.route]: undefined;
[DESIGN_SYSTEM_ROUTES.COMPONENTS
.HEADER_SECOND_LEVEL_DISCRETE_TRANSITION_CUSTOM_BG.route]: undefined;
[DESIGN_SYSTEM_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_CUSTOM_BG
.route]: undefined;
[DESIGN_SYSTEM_ROUTES.COMPONENTS.HEADER_SECOND_LEVEL_STATIC.route]: undefined;
Expand Down
8 changes: 8 additions & 0 deletions example/src/navigation/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ const APP_ROUTES = {
route: "DESIGN_SYSTEM_HEADER_SECOND_LEVEL",
title: "Header Second Level"
},
HEADER_SECOND_LEVEL_DISCRETE_TRANSITION: {
route: "DESIGN_SYSTEM_HEADER_SECOND_LEVEL_DISCRETE_TRANSITION",
title: "Header Second Level (Discrete transition, default)"
},
HEADER_SECOND_LEVEL_DISCRETE_TRANSITION_CUSTOM_BG: {
route: "DESIGN_SYSTEM_HEADER_SECOND_LEVEL_DISCRETE_TRANSITION_CUSTOM_BG",
title: "Header Second Level (Discrete transition, custom background)"
},
HEADER_SECOND_LEVEL_CUSTOM_BG: {
route: "DESIGN_SYSTEM_HEADER_SECOND_LEVEL_CUSTOM_BG",
title: "Header Second Level (Custom Background)"
Expand Down
73 changes: 73 additions & 0 deletions example/src/pages/HeaderSecondLevelDiscreteTransition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
Body,
H3,
HeaderSecondLevel,
IOVisualCostants,
VSpacer
} from "@pagopa/io-app-design-system";
import { useNavigation } from "@react-navigation/native";
import * as React from "react";
import { useLayoutEffect } from "react";
import { Alert } from "react-native";
import Animated, { useAnimatedRef } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";

export const HeaderSecondLevelScreenDiscreteTransition = () => {
const insets = useSafeAreaInsets();
const navigation = useNavigation();

const animatedScrollViewRef = useAnimatedRef<Animated.ScrollView>();

useLayoutEffect(() => {
navigation.setOptions({
header: () => (
<HeaderSecondLevel
enableDiscreteTransition
animatedRef={animatedScrollViewRef as any}
title={"Questo è un titolo lungo, ma lungo lungo davvero, eh!"}
goBack={() => navigation.goBack()}
backAccessibilityLabel="Torna indietro"
type="threeActions"
firstAction={{
icon: "help",
onPress: () => {
Alert.alert("Contextual Help");
},
accessibilityLabel: ""
}}
secondAction={{
icon: "add",
onPress: () => {
Alert.alert("add");
},
accessibilityLabel: ""
}}
thirdAction={{
icon: "coggle",
onPress: () => {
Alert.alert("Settings");
},
accessibilityLabel: ""
}}
/>
)
});
}, [animatedScrollViewRef, navigation]);

return (
<Animated.ScrollView
ref={animatedScrollViewRef}
contentContainerStyle={{
paddingBottom: insets.bottom,
paddingHorizontal: IOVisualCostants.appMarginDefault
}}
scrollEventThrottle={8}
>
<H3>Questo è un titolo lungo, ma lungo lungo davvero, eh!</H3>
<VSpacer />
{[...Array(50)].map((_el, i) => (
<Body key={`body-${i}`}>Repeated text</Body>
))}
</Animated.ScrollView>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
Body,
H3,
HeaderSecondLevel,
IOColors,
IOVisualCostants,
VSpacer
} from "@pagopa/io-app-design-system";
import { useNavigation } from "@react-navigation/native";
import { useHeaderHeight } from "@react-navigation/elements";
import * as React from "react";
import { useLayoutEffect } from "react";
import { Alert, View } from "react-native";
import Animated, { useAnimatedRef } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";

export const HeaderSecondLevelScreenDiscreteTransitionCustomBg = () => {
const insets = useSafeAreaInsets();
const navigation = useNavigation();
const headerHeight = useHeaderHeight();

const animatedScrollViewRef = useAnimatedRef<Animated.ScrollView>();

useLayoutEffect(() => {
navigation.setOptions({
headerTransparent: true,
header: () => (
<HeaderSecondLevel
enableDiscreteTransition
transparent
variant="contrast"
backgroundColor={IOColors["blueIO-500"]}
animatedRef={animatedScrollViewRef as any}
title={"Questo è un titolo lungo, ma lungo lungo davvero, eh!"}
goBack={() => navigation.goBack()}
backAccessibilityLabel="Torna indietro"
type="threeActions"
firstAction={{
icon: "help",
onPress: () => {
Alert.alert("Contextual Help");
},
accessibilityLabel: ""
}}
secondAction={{
icon: "add",
onPress: () => {
Alert.alert("add");
},
accessibilityLabel: ""
}}
thirdAction={{
icon: "coggle",
onPress: () => {
Alert.alert("Settings");
},
accessibilityLabel: ""
}}
/>
)
});
}, [animatedScrollViewRef, navigation]);

return (
<Animated.ScrollView
ref={animatedScrollViewRef}
contentContainerStyle={{
paddingTop: headerHeight,
paddingBottom: insets.bottom,
paddingHorizontal: IOVisualCostants.appMarginDefault
}}
scrollEventThrottle={8}
>
<View
style={{
backgroundColor: IOColors["blueIO-600"],
height: 800,
position: "absolute",
top: -400,
left: 0,
right: 0
}}
/>

<H3 color="white">
Questo è un titolo lungo, ma lungo lungo davvero, eh!
</H3>
<VSpacer />
{[...Array(50)].map((_el, i) => (
<Body key={`body-${i}`}>Repeated text</Body>
))}
</Animated.ScrollView>
);
};
59 changes: 51 additions & 8 deletions src/components/layout/HeaderSecondLevel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { ComponentProps } from "react";
import { ComponentProps, useEffect, useLayoutEffect } from "react";
import {
AccessibilityInfo,
ColorValue,
Expand All @@ -9,15 +9,21 @@ import {
findNodeHandle
} from "react-native";
import Animated, {
AnimatedRef,
SharedValue,
interpolate,
interpolateColor,
useAnimatedStyle,
useDerivedValue,
useScrollViewOffset,
useSharedValue,
withSpring,
withTiming
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
IOColors,
IOSpringValues,
IOStyles,
IOVisualCostants,
alertEdgeToEdgeInsetTransitionConfig,
Expand All @@ -34,10 +40,20 @@ import { HSpacer } from "../spacer";
import { ActionProp } from "./common";

type ScrollValues = {
contentOffsetY: Animated.SharedValue<number>;
contentOffsetY: SharedValue<number>;
triggerOffset: number;
};

type DiscreteTransitionProps =
| {
enableDiscreteTransition: true;
animatedRef: AnimatedRef<Animated.ScrollView>;
}
| {
enableDiscreteTransition?: false;
animatedRef?: never;
};

type BackProps =
| {
goBack: () => void;
Expand Down Expand Up @@ -89,6 +105,7 @@ interface ThreeActions extends CommonProps {
}

export type HeaderSecondLevel = BackProps &
DiscreteTransitionProps &
(Base | OneAction | TwoActions | ThreeActions);

const titleHorizontalMargin: IOSpacingScale = 16;
Expand Down Expand Up @@ -135,11 +152,16 @@ export const HeaderSecondLevel = ({
backgroundColor,
transparent = false,
ignoreSafeAreaMargin = false,
enableDiscreteTransition = false,
animatedRef,
testID,
firstAction,
secondAction,
thirdAction
}: HeaderSecondLevel) => {
const scrollOffset = useScrollViewOffset(
animatedRef as AnimatedRef<Animated.ScrollView>
);
const titleRef = React.createRef<View>();

const { isExperimental } = useIOExperimentalDesign();
Expand All @@ -160,7 +182,10 @@ export const HeaderSecondLevel = ({

const headerBgColorTransparentState = backgroundColor
? hexToRgba(backgroundColor, 0)
: hexToRgba(IOColors[HEADER_DEFAULT_BG_COLOR], 0);
: transparent
? hexToRgba(IOColors[HEADER_DEFAULT_BG_COLOR], 0)
: IOColors[HEADER_DEFAULT_BG_COLOR];

const headerBgColorSolidState =
backgroundColor ?? IOColors[HEADER_DEFAULT_BG_COLOR];

Expand All @@ -169,7 +194,7 @@ export const HeaderSecondLevel = ({
: hexToRgba(IOColors["grey-100"], 0);
const borderColorSolidState = backgroundColor ?? IOColors["grey-100"];

React.useLayoutEffect(() => {
useLayoutEffect(() => {
if (isTitleAccessible) {
const reactNode = findNodeHandle(titleRef.current);
if (reactNode !== null) {
Expand All @@ -178,7 +203,11 @@ export const HeaderSecondLevel = ({
}
});

React.useEffect(() => {
const bgColorDiscreteTransition = useDerivedValue(() =>
withSpring(scrollOffset.value > 0 ? 1 : 0, IOSpringValues.header)
);

useEffect(() => {
// eslint-disable-next-line functional/immutable-data
paddingTop.value = withTiming(
ignoreSafeAreaMargin ? 0 : insets.top,
Expand All @@ -191,14 +220,26 @@ export const HeaderSecondLevel = ({
}));

const headerWrapperAnimatedStyle = useAnimatedStyle(() => ({
backgroundColor: scrollValues
backgroundColor: enableDiscreteTransition
? interpolateColor(
bgColorDiscreteTransition.value,
[0, 1],
[headerBgColorTransparentState, headerBgColorSolidState]
)
: scrollValues
? interpolateColor(
scrollValues.contentOffsetY.value,
[0, scrollValues.triggerOffset],
[headerBgColorTransparentState, headerBgColorSolidState]
)
: headerBgColorSolidState,
borderColor: scrollValues
borderColor: enableDiscreteTransition
? interpolateColor(
bgColorDiscreteTransition.value,
[0, 1],
[borderColorTransparentState, borderColorSolidState]
)
: scrollValues
? interpolateColor(
scrollValues.contentOffsetY.value,
[0, scrollValues.triggerOffset],
Expand All @@ -208,7 +249,9 @@ export const HeaderSecondLevel = ({
}));

const titleAnimatedStyle = useAnimatedStyle(() => ({
opacity: scrollValues
opacity: enableDiscreteTransition
? interpolate(bgColorDiscreteTransition.value, [0, 1], [0, 1])
: scrollValues
? interpolate(
scrollValues.contentOffsetY.value,
[0, scrollValues.triggerOffset],
Expand Down
Loading

0 comments on commit 82f79fb

Please sign in to comment.