Skip to content

Commit

Permalink
Merge pull request #241 from wojciech-deriv/feature/UPM-1401-notifica…
Browse files Browse the repository at this point in the history
…tions-infinite-scroll

[UPM-1401] added infinite scroll to the notifications for p2p
  • Loading branch information
azmib-developer authored Aug 27, 2024
2 parents 3f9fecf + acf7c1c commit fbf33b9
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { render } from "@testing-library/react";
import { render, waitFor } from "@testing-library/react";
import { Notifications } from "..";
import userEvent from "@testing-library/user-event";

// Mocking the useDevice hook
jest.mock("../../../../hooks", () => ({
useDevice: jest.fn().mockReturnValue({ isMobile: false }),
}));

function generateNotification(idx: number, action: () => void) {
return {
icon: <span>Icon{idx}</span>,
title: `Title ${idx}`,
message: `Message ${idx}`,
buttonAction: action || jest.fn(),
actionText: `Action ${idx}`,
};
}

describe("Notifications Component", () => {
it("should show no notifications when notifications array is empty", () => {
const { queryByText } = render(
Expand All @@ -14,6 +25,8 @@ describe("Notifications Component", () => {
clearNotificationsCallback={() => {}}
isOpen={true}
setIsOpen={() => {}}
loadMoreFunction={() => {}}
isLoading={false}
componentConfig={{
clearButtonText: "Clear all",
modalTitle: "Notifications",
Expand All @@ -28,20 +41,8 @@ describe("Notifications Component", () => {
const mockAction1 = jest.fn();
const mockAction2 = jest.fn();
const notifications = [
{
icon: <span>Icon1</span>,
title: "Title 1",
message: "Message 1",
buttonAction: mockAction1,
actionText: "Action 1",
},
{
icon: <span>Icon2</span>,
title: "Title 2",
message: "Message 2",
buttonAction: mockAction2,
actionText: "Action 2",
},
generateNotification(1, mockAction1),
generateNotification(2, mockAction2),
];

const { getByText, getAllByRole } = render(
Expand All @@ -50,6 +51,8 @@ describe("Notifications Component", () => {
clearNotificationsCallback={() => {}}
isOpen={true}
setIsOpen={() => {}}
loadMoreFunction={() => {}}
isLoading={false}
componentConfig={{
clearButtonText: "Clear all",
modalTitle: "Notifications",
Expand All @@ -73,4 +76,55 @@ describe("Notifications Component", () => {
await userEvent.click(buttons[1]);
expect(mockAction2).toHaveBeenCalled();
});

it('displays a loading spinner when "isLoading" is true', async() => {
const { queryByTestId } = render(
<Notifications
notifications={[]}
clearNotificationsCallback={() => {}}
isOpen={true}
setIsOpen={() => {}}
loadMoreFunction={() => {}}
isLoading={true}
componentConfig={{
clearButtonText: "Clear all",
modalTitle: "Notifications",
noNotificationsTitle: "No notifications",
noNotificationsMessage: "You have no notifications",
}}
/>,
);
expect(queryByTestId("notifications-loader")).toBeInTheDocument();
});

it('calls the "loadMoreFunction" when content is scrolled to the bottom', async() => {
const mockLoadMore = jest.fn();

const notifications = Array.from({ length: 20 }, (_, idx) => generateNotification(idx, jest.fn()));

const { getByTestId } = render(
<Notifications
notifications={notifications}
clearNotificationsCallback={() => {}}
isOpen={true}
setIsOpen={() => {}}
loadMoreFunction={mockLoadMore}
isLoading={false}
componentConfig={{
clearButtonText: "Clear all",
modalTitle: "Notifications",
noNotificationsTitle: "No notifications",
noNotificationsMessage: "You have no notifications",
}}
/>,
);

const content = getByTestId("notifications-content");
content.scrollTop = content.scrollHeight;
content.dispatchEvent(new Event("scroll"));
// Wait for any asynchronous effects
await waitFor(() => {
expect(mockLoadMore).toHaveBeenCalled();
});
});
});
9 changes: 9 additions & 0 deletions src/components/AppLayout/Notifications/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
0px 0px 16px 0px rgba(0, 0, 0, 0.08),
0px 16px 16px 0px rgba(0, 0, 0, 0.08);
min-height: 562px !important;
height: 562px;
display: flex;
flex-direction: column;
&__empty {
Expand Down Expand Up @@ -101,6 +102,14 @@
border-bottom: 1px solid var(--du-border-normal, #d6dadb);
}
}
&__content {
overflow-y: auto;
}
&__loader {
display: flex;
justify-content: center;
align-items: center;
}
&__footer {
align-items: flex-end;
font-size: 12px;
Expand Down
60 changes: 48 additions & 12 deletions src/components/AppLayout/Notifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Text } from "../../Text";
import { useOnClickOutside } from "usehooks-ts";
import Icon from "./ic-box.svg";
import clsx from "clsx";
import { useFetchMore } from "../../../hooks/useFetchMore";
import { Loader } from "../../Loader";

export const Notifications = ({
notifications,
Expand All @@ -17,16 +19,24 @@ export const Notifications = ({
setIsOpen,
componentConfig,
className,
loadMoreFunction,
isLoading,
...rest
}: Omit<TNotificationsProps, "style">) => {
const { isMobile } = useDevice();
const notificationsRef = useRef(null);
const notificationsScrollRef = useRef(null);

useOnClickOutside(notificationsRef, (e) => {
e.stopPropagation();
setIsOpen(false);
});

const { fetchMoreOnBottomReached } = useFetchMore({
loadMore: loadMoreFunction,
ref: notificationsRef,
});

return (
<Fragment>
{isMobile && (
Expand Down Expand Up @@ -66,12 +76,24 @@ export const Notifications = ({
</Text>
</div>
)}
{notifications.map((notification) => (
<Notification
key={notification.title}
{...notification}
/>
))}
<div
className="notifications__content"
ref={notificationsScrollRef}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
data-testid="notifications-content"
>
{notifications.map((notification) => (
<Notification
key={notification.title}
{...notification}
/>
))}
{isLoading && (
<div className="notifications__loader" data-testid="notifications-loader">
<Loader isFullScreen={false}/>
</div>
)}
</div>
<Modal.Footer className="notifications__footer">
<button
className={clsx("notifications__footer__clear-button", {
Expand All @@ -89,6 +111,8 @@ export const Notifications = ({
</Modal.Footer>
</Modal>
)}


{!isMobile && (
<ContextMenu
ref={notificationsRef}
Expand All @@ -114,12 +138,24 @@ export const Notifications = ({
</Text>
</div>
)}
{notifications.map((notification) => (
<Notification
key={notification.title}
{...notification}
/>
))}
<div
className="notifications__content"
ref={notificationsScrollRef}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
data-testid="notifications-content"
>
{notifications.map((notification) => (
<Notification
key={notification.title}
{...notification}
/>
))}
{isLoading && (
<div className="notifications__loader" data-testid="notifications-loader">
<Loader isFullScreen={false}/>
</div>
)}
</div>
<div className="notifications__footer">
<div className="notifications__footer-box">
<button
Expand Down
2 changes: 2 additions & 0 deletions src/components/AppLayout/Notifications/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type TNotificationsProps = ComponentProps<"div"> & {
clearNotificationsCallback: () => void;
setIsOpen: (state: boolean) => void;
isOpen: boolean;
loadMoreFunction: () => void;
isLoading: boolean;
componentConfig: {
clearButtonText: string;
modalTitle: string;
Expand Down
Loading

0 comments on commit fbf33b9

Please sign in to comment.