diff --git a/ts/components/ForceScrollDownView.tsx b/ts/components/ForceScrollDownView.tsx deleted file mode 100644 index 97363012304..00000000000 --- a/ts/components/ForceScrollDownView.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { IOSpringValues, IconButtonSolid } from "@pagopa/io-app-design-system"; -import { - ReactNode, - useCallback, - useEffect, - useMemo, - useRef, - useState -} from "react"; -import { - LayoutChangeEvent, - NativeScrollEvent, - NativeSyntheticEvent, - ScrollView, - ScrollViewProps, - StyleSheet -} from "react-native"; -import { ScaleInOutAnimation } from "./animations/ScaleInOutAnimation"; - -type ForceScrollDownViewProps = { - /** - * The content to display inside the scroll view. - */ - children: ReactNode; - /** - * The distance from the bottom of the scrollable content at which the "scroll to bottom" button - * should become hidden. Defaults to 100. - */ - threshold?: number; - /** - * A callback that will be called whenever the scroll view crosses the threshold. The callback - * is passed a boolean indicating whether the threshold has been crossed (`true`) or not (`false`). - */ - onThresholdCrossed?: (crossed: boolean) => void; -} & Pick< - ScrollViewProps, - "style" | "contentContainerStyle" | "scrollEnabled" | "testID" ->; - -/** - * A React Native component that displays a scroll view with a button that scrolls to the bottom of the content - * when pressed. The button is hidden when the scroll view reaches a certain threshold from the bottom, which is - * configurable by the `threshold` prop. The button, and the scrolling, can also be disabled by setting the - * `scrollEnabled` prop to `false`. - */ -const ForceScrollDownView = ({ - children, - threshold = 100, - style, - contentContainerStyle, - scrollEnabled = true, - onThresholdCrossed -}: ForceScrollDownViewProps) => { - const scrollViewRef = useRef(null); - - /** - * The height of the scroll view, used to determine whether or not the scrollable content fits inside - * the scroll view and whether the "scroll to bottom" button should be displayed. - */ - const [scrollViewHeight, setScrollViewHeight] = useState(); - - /** - * The height of the scrollable content, used to determine whether or not the "scroll to bottom" button - * should be displayed. - */ - const [contentHeight, setContentHeight] = useState(); - - /** - * Whether or not the scroll view has crossed the threshold from the bottom. - */ - const [isThresholdCrossed, setThresholdCrossed] = useState(false); - - /** - * Whether or not the "scroll to bottom" button should be visible. This is controlled by the threshold - * and the current scroll position. - */ - const [isButtonVisible, setButtonVisible] = useState(true); - - /** - * A callback that is called whenever the scroll view is scrolled. It checks whether or not the - * scroll view has crossed the threshold from the bottom and updates the state accordingly. - * The callback is designed to updatr button visibility only when crossing the threshold. - */ - const handleScroll = useCallback( - (event: NativeSyntheticEvent) => { - const { layoutMeasurement, contentOffset, contentSize } = - event.nativeEvent; - - const thresholdCrossed = - layoutMeasurement.height + contentOffset.y >= - contentSize.height - threshold; - - setThresholdCrossed(previousState => { - if (!previousState && thresholdCrossed) { - setButtonVisible(false); - } - if (previousState && !thresholdCrossed) { - setButtonVisible(true); - } - return thresholdCrossed; - }); - }, - [threshold] - ); - - /** - * A side effect that calls the `onThresholdCrossed` callback whenever the value of `isThresholdCrossed` changes. - */ - useEffect(() => { - onThresholdCrossed?.(isThresholdCrossed); - }, [onThresholdCrossed, isThresholdCrossed]); - - /** - * A callback that is called whenever the size of the scrollable content changes. It updates the - * state with the new content height. - */ - const handleContentSizeChange = useCallback( - (_contentWidth: number, contentHeight: number) => { - setContentHeight(contentHeight); - }, - [] - ); - - /** - * A callback that is called whenever the size of the scroll view changes. It updates the state - * with the new scroll view height. - */ - const handleLayout = useCallback((event: LayoutChangeEvent) => { - setScrollViewHeight(event.nativeEvent.layout.height); - }, []); - - /** - * A callback that is called when the "scroll to bottom" button is pressed. It scrolls the - * scroll view to the bottom and hides the button. - */ - const handleScrollDownPress = useCallback(() => { - setButtonVisible(false); - scrollViewRef.current?.scrollToEnd(); - }, [scrollViewRef]); - - /** - * Whether or not the "scroll to bottom" button needs to be displayed. It is only displayed - * when the scrollable content cannot fit inside the scroll view and the button is enabled - * (`scrollEnabled` is `true`). - */ - const needsScroll = useMemo( - () => - scrollViewHeight != null && - contentHeight != null && - scrollViewHeight < contentHeight, - [scrollViewHeight, contentHeight] - ); - - /** - * Whether or not to render the "scroll to bottom" button. It is only rendered when the scroll view - * is enabled, needs to be scrolled, and the button is visible (`isButtonVisible` is `true`). - */ - const shouldRenderScrollButton = - scrollEnabled && needsScroll && isButtonVisible; - - /** - * The "scroll to bottom" button component. It is wrapped in a reanimated view and has enter and exit - * animations applied to it. - */ - const scrollDownButton = ( - - - - ); - - return ( - <> - - {children} - - {scrollDownButton} - - ); -}; - -const styles = StyleSheet.create({ - scrollDownButton: { - position: "absolute", - zIndex: 10, - right: 20, - bottom: 50 - } -}); - -export { ForceScrollDownView }; diff --git a/ts/components/__tests__/ForceScrollDownView.test.tsx b/ts/components/__tests__/ForceScrollDownView.test.tsx deleted file mode 100644 index 9138141b10a..00000000000 --- a/ts/components/__tests__/ForceScrollDownView.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { fireEvent, render } from "@testing-library/react-native"; -import { PropsWithChildren, ReactElement } from "react"; - -import { Text } from "react-native"; -import { Provider } from "react-redux"; -import { createStore } from "redux"; -import { applicationChangeState } from "../../store/actions/application"; -import { appReducer } from "../../store/reducers"; -import { ForceScrollDownView } from "../ForceScrollDownView"; - -describe("ForceScrollDownView", () => { - jest.useFakeTimers(); - - it("renders the content correctly", () => { - const tContent = "Some content"; - const tChildren = {tContent}; - - const { getByText } = renderComponent( - {tChildren} - ); - - expect(getByText(tContent)).toBeDefined(); - }); - - it("displays the scroll down button when necessary", async () => { - const tContent = "Some content"; - const tChildren = {tContent}; - - const tScreenHeight = 1000; - - const { getByTestId, queryByTestId } = renderComponent( - {tChildren} - ); - - const scrollView = getByTestId("ScrollView"); - - // Update scroll view height - fireEvent(scrollView, "layout", { - nativeEvent: { - layout: { - height: tScreenHeight - } - } - }); - - // Update scroll view content height - fireEvent(scrollView, "contentSizeChange", null, tScreenHeight - 500); - - // Button should not be visible because content does not need scrolling - const buttonBefore = queryByTestId("ScrollDownButton"); - expect(buttonBefore).toBeNull(); - - // Increase content height to force button to be shown - fireEvent(scrollView, "contentSizeChange", null, tScreenHeight + 500); - - jest.advanceTimersByTime(500); - - // Button should be visible now beacuse content needs scrolling - const buttonAfter = queryByTestId("ScrollDownButton"); - expect(buttonAfter).not.toBeNull(); - }); - - it("scrolls to the bottom when the button is pressed", () => { - const tContent = "Some content"; - const tChildren = {tContent}; - - const tScreenHeight = 1000; - - const { getByTestId, queryByTestId } = renderComponent( - {tChildren} - ); - - const scrollView = getByTestId("ScrollView"); - - // Update scroll view height - fireEvent(scrollView, "layout", { - nativeEvent: { - layout: { - height: tScreenHeight - } - } - }); - - // Update scroll view content height - fireEvent(scrollView, "contentSizeChange", null, tScreenHeight + 500); - - // Button should be visible - const buttonBefore = getByTestId("ScrollDownButton"); - expect(buttonBefore).not.toBeNull(); - - // Fire button press event - fireEvent.press(buttonBefore); - - // Wait for the scroll animation - jest.advanceTimersByTime(500); - - // Button should not be visible after scrolling - const buttonAfter = queryByTestId("ScrollDownButton"); - expect(buttonAfter).toBeNull(); - }); -}); - -export const renderComponent = (component: ReactElement) => { - const globalState = appReducer(undefined, applicationChangeState("active")); - const store = createStore(appReducer, globalState as any); - - const Wrapper = ({ children }: PropsWithChildren) => ( - {children} - ); - - return render(component, { - wrapper: Wrapper - }); -}; diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index af04f4c17b9..059c99ad16d 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -1,10 +1,13 @@ -import { FooterActions, VSpacer } from "@pagopa/io-app-design-system"; +import { + FooterActions, + ForceScrollDownView, + VSpacer +} from "@pagopa/io-app-design-system"; import { RouteProp, useRoute } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { useEffect } from "react"; import { StyleSheet, View } from "react-native"; -import { ForceScrollDownView } from "../../../../components/ForceScrollDownView"; import IOMarkdown from "../../../../components/IOMarkdown"; import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel";